@questionbase/deskfree 0.6.1 → 0.6.3
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/bin.js +783 -521
- package/dist/bin.js.map +1 -1
- package/dist/cli/install.d.ts +1 -1
- package/dist/cli/install.js +11 -19
- package/dist/cli/install.js.map +1 -1
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10508 -10171
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/bin.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { homedir } from 'os';
|
|
3
|
+
import { totalmem, cpus, hostname, arch, platform, homedir } from 'os';
|
|
4
4
|
import { dirname, join, extname, basename } from 'path';
|
|
5
5
|
import { spawn, execSync, execFileSync, execFile } from 'child_process';
|
|
6
|
-
import { mkdirSync, writeFileSync, chmodSync,
|
|
6
|
+
import { mkdirSync, readFileSync, existsSync, writeFileSync, chmodSync, unlinkSync, appendFileSync, statSync, createWriteStream } from 'fs';
|
|
7
7
|
import { createRequire as createRequire$1 } from 'module';
|
|
8
8
|
import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -109,7 +109,7 @@ var install_exports = {};
|
|
|
109
109
|
__export(install_exports, {
|
|
110
110
|
install: () => install
|
|
111
111
|
});
|
|
112
|
-
function installMac(
|
|
112
|
+
function installMac(botId, name2, stage) {
|
|
113
113
|
const paths = getMacPaths(name2);
|
|
114
114
|
const plistLabel = getPlistLabel(name2);
|
|
115
115
|
let nodeBinDir;
|
|
@@ -125,13 +125,9 @@ function installMac(token, name2) {
|
|
|
125
125
|
mkdirSync(paths.deskfreeDir, { recursive: true });
|
|
126
126
|
mkdirSync(paths.logDir, { recursive: true });
|
|
127
127
|
mkdirSync(dirname(paths.plist), { recursive: true });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
DESKFREE_INSTANCE_NAME=${name2}
|
|
132
|
-
`,
|
|
133
|
-
{ mode: 384 }
|
|
134
|
-
);
|
|
128
|
+
const envLines = [`BOT=${botId}`, `DESKFREE_INSTANCE_NAME=${name2}`];
|
|
129
|
+
if (stage) envLines.push(`STAGE=${stage}`);
|
|
130
|
+
writeFileSync(paths.envFile, envLines.join("\n") + "\n", { mode: 384 });
|
|
135
131
|
chmodSync(paths.envFile, 384);
|
|
136
132
|
console.log(`Wrote ${paths.envFile}`);
|
|
137
133
|
const launcher = `#!/bin/bash
|
|
@@ -189,7 +185,7 @@ Service ${plistLabel} installed and started.`);
|
|
|
189
185
|
console.log(`Check status: launchctl print gui/$(id -u)/${plistLabel}`);
|
|
190
186
|
console.log(`Logs: tail -f ${join(paths.logDir, "stdout.log")}`);
|
|
191
187
|
}
|
|
192
|
-
function installLinux(
|
|
188
|
+
function installLinux(botId, name2, stage) {
|
|
193
189
|
if (process.getuid?.() !== 0) {
|
|
194
190
|
console.error("Error: install must be run as root (use sudo)");
|
|
195
191
|
process.exit(1);
|
|
@@ -220,13 +216,9 @@ function installLinux(token, name2) {
|
|
|
220
216
|
`chown ${systemUser}:${systemUser} ${paths.stateDir} ${paths.logDir}`
|
|
221
217
|
);
|
|
222
218
|
console.log(`Created ${paths.stateDir} and ${paths.logDir}`);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
DESKFREE_INSTANCE_NAME=${name2}
|
|
227
|
-
`,
|
|
228
|
-
{ mode: 384 }
|
|
229
|
-
);
|
|
219
|
+
const envLines = [`BOT=${botId}`, `DESKFREE_INSTANCE_NAME=${name2}`];
|
|
220
|
+
if (stage) envLines.push(`STAGE=${stage}`);
|
|
221
|
+
writeFileSync(paths.envFile, envLines.join("\n") + "\n", { mode: 384 });
|
|
230
222
|
chmodSync(paths.envFile, 384);
|
|
231
223
|
console.log(`Wrote ${paths.envFile}`);
|
|
232
224
|
const unit = `[Unit]
|
|
@@ -264,11 +256,11 @@ Service ${serviceName} installed and started.`);
|
|
|
264
256
|
console.log(`Check status: systemctl status ${serviceName}`);
|
|
265
257
|
console.log(`Logs: tail -f ${paths.logDir}/stdout.log`);
|
|
266
258
|
}
|
|
267
|
-
function install(
|
|
259
|
+
function install(botId, name2, stage) {
|
|
268
260
|
if (process.platform === "darwin") {
|
|
269
|
-
installMac(
|
|
261
|
+
installMac(botId, name2, stage);
|
|
270
262
|
} else if (process.platform === "linux") {
|
|
271
|
-
installLinux(
|
|
263
|
+
installLinux(botId, name2, stage);
|
|
272
264
|
} else {
|
|
273
265
|
console.error(`Unsupported platform: ${process.platform}`);
|
|
274
266
|
process.exit(1);
|
|
@@ -2649,9 +2641,6 @@ function validateStringParam(params, key, required) {
|
|
|
2649
2641
|
}
|
|
2650
2642
|
function validateEnumParam(params, key, values, required) {
|
|
2651
2643
|
const value = params?.[key];
|
|
2652
|
-
if (required && (value === void 0 || value === null)) {
|
|
2653
|
-
throw new Error(`Missing required parameter: ${key}`);
|
|
2654
|
-
}
|
|
2655
2644
|
if (value !== void 0 && value !== null && !values.includes(value)) {
|
|
2656
2645
|
throw new Error(
|
|
2657
2646
|
`Parameter ${key} must be one of: ${values.join(", ")}. Got: ${value}`
|
|
@@ -2740,34 +2729,33 @@ function createOrchestratorTools(client, _options) {
|
|
|
2740
2729
|
return errorResult(err);
|
|
2741
2730
|
}
|
|
2742
2731
|
}),
|
|
2743
|
-
createTool(ORCHESTRATOR_TOOLS.
|
|
2732
|
+
createTool(ORCHESTRATOR_TOOLS.CREATE_TASK, async (params) => {
|
|
2744
2733
|
try {
|
|
2745
|
-
const
|
|
2746
|
-
const
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
taskId
|
|
2734
|
+
const title = validateStringParam(params, "title", true);
|
|
2735
|
+
const instructions = validateStringParam(params, "instructions", false);
|
|
2736
|
+
const suggestedByTaskId = validateStringParam(
|
|
2737
|
+
params,
|
|
2738
|
+
"suggestedByTaskId",
|
|
2739
|
+
false
|
|
2740
|
+
);
|
|
2741
|
+
const inputFileIds = Array.isArray(params.inputFileIds) ? params.inputFileIds : void 0;
|
|
2742
|
+
const outputFileIds = Array.isArray(params.outputFileIds) ? params.outputFileIds : void 0;
|
|
2743
|
+
const scheduledFor = validateStringParam(params, "scheduledFor", false);
|
|
2744
|
+
const estimatedTokens = typeof params.estimatedTokens === "number" ? params.estimatedTokens : void 0;
|
|
2745
|
+
const result = await client.createTask({
|
|
2746
|
+
title,
|
|
2747
|
+
instructions,
|
|
2748
|
+
estimatedTokens,
|
|
2749
|
+
scheduledFor,
|
|
2750
|
+
inputFileIds,
|
|
2751
|
+
outputFileIds,
|
|
2752
|
+
suggestedByTaskId
|
|
2765
2753
|
});
|
|
2766
2754
|
return {
|
|
2767
2755
|
content: [
|
|
2768
2756
|
{
|
|
2769
2757
|
type: "text",
|
|
2770
|
-
text: `
|
|
2758
|
+
text: `Task created: "${result.title}" (${result.taskId})`
|
|
2771
2759
|
}
|
|
2772
2760
|
]
|
|
2773
2761
|
};
|
|
@@ -2787,15 +2775,36 @@ function createOrchestratorTools(client, _options) {
|
|
|
2787
2775
|
return errorResult(err);
|
|
2788
2776
|
}
|
|
2789
2777
|
}),
|
|
2790
|
-
createTool(ORCHESTRATOR_TOOLS.
|
|
2778
|
+
createTool(ORCHESTRATOR_TOOLS.UPDATE_TASK_STATUS, async (params) => {
|
|
2791
2779
|
try {
|
|
2792
2780
|
const taskId = validateStringParam(params, "taskId", true);
|
|
2781
|
+
const status2 = validateEnumParam(
|
|
2782
|
+
params,
|
|
2783
|
+
"status",
|
|
2784
|
+
["open", "done"],
|
|
2785
|
+
false
|
|
2786
|
+
);
|
|
2787
|
+
const awaiting = validateEnumParam(
|
|
2788
|
+
params,
|
|
2789
|
+
"awaiting",
|
|
2790
|
+
["bot", "human"],
|
|
2791
|
+
false
|
|
2792
|
+
);
|
|
2793
2793
|
const reason = validateStringParam(params, "reason", false);
|
|
2794
|
-
const result = await client.
|
|
2794
|
+
const result = await client.updateTaskStatus({
|
|
2795
|
+
taskId,
|
|
2796
|
+
status: status2,
|
|
2797
|
+
awaiting,
|
|
2798
|
+
reason
|
|
2799
|
+
});
|
|
2800
|
+
const details = [];
|
|
2801
|
+
if (status2) details.push(`Status: ${status2}`);
|
|
2802
|
+
if (awaiting) details.push(`Awaiting: ${awaiting}`);
|
|
2803
|
+
if (reason) details.push(`Reason: ${reason}`);
|
|
2795
2804
|
return formatTaskResponse(
|
|
2796
2805
|
result,
|
|
2797
|
-
`Task "${result.title}"
|
|
2798
|
-
|
|
2806
|
+
`Task "${result.title}" updated`,
|
|
2807
|
+
details
|
|
2799
2808
|
);
|
|
2800
2809
|
} catch (err) {
|
|
2801
2810
|
return errorResult(err);
|
|
@@ -2867,18 +2876,7 @@ function createWorkerTools(client, options) {
|
|
|
2867
2876
|
try {
|
|
2868
2877
|
const content = validateStringParam(params, "content", true);
|
|
2869
2878
|
const taskId = validateStringParam(params, "taskId", false);
|
|
2870
|
-
const type = validateEnumParam(params, "type", ["notify", "ask"], true);
|
|
2871
2879
|
await client.sendMessage({ content, taskId });
|
|
2872
|
-
if (type === "ask") {
|
|
2873
|
-
return {
|
|
2874
|
-
content: [
|
|
2875
|
-
{
|
|
2876
|
-
type: "text",
|
|
2877
|
-
text: "Ask sent \u2014 task is now awaiting human response. Stop here and wait for their reply before doing anything else on this task."
|
|
2878
|
-
}
|
|
2879
|
-
]
|
|
2880
|
-
};
|
|
2881
|
-
}
|
|
2882
2880
|
return {
|
|
2883
2881
|
content: [{ type: "text", text: "Message sent successfully" }]
|
|
2884
2882
|
};
|
|
@@ -2975,34 +2973,33 @@ function createWorkerTools(client, options) {
|
|
|
2975
2973
|
return errorResult(err);
|
|
2976
2974
|
}
|
|
2977
2975
|
}),
|
|
2978
|
-
createTool(WORKER_TOOLS.
|
|
2976
|
+
createTool(WORKER_TOOLS.CREATE_TASK, async (params) => {
|
|
2979
2977
|
try {
|
|
2980
|
-
const
|
|
2981
|
-
const
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
taskId
|
|
2978
|
+
const title = validateStringParam(params, "title", true);
|
|
2979
|
+
const instructions = validateStringParam(params, "instructions", false);
|
|
2980
|
+
const suggestedByTaskId = validateStringParam(
|
|
2981
|
+
params,
|
|
2982
|
+
"suggestedByTaskId",
|
|
2983
|
+
false
|
|
2984
|
+
);
|
|
2985
|
+
const inputFileIds = Array.isArray(params.inputFileIds) ? params.inputFileIds : void 0;
|
|
2986
|
+
const outputFileIds = Array.isArray(params.outputFileIds) ? params.outputFileIds : void 0;
|
|
2987
|
+
const scheduledFor = validateStringParam(params, "scheduledFor", false);
|
|
2988
|
+
const estimatedTokens = typeof params.estimatedTokens === "number" ? params.estimatedTokens : void 0;
|
|
2989
|
+
const result = await client.createTask({
|
|
2990
|
+
title,
|
|
2991
|
+
instructions,
|
|
2992
|
+
estimatedTokens,
|
|
2993
|
+
scheduledFor,
|
|
2994
|
+
inputFileIds,
|
|
2995
|
+
outputFileIds,
|
|
2996
|
+
suggestedByTaskId
|
|
3000
2997
|
});
|
|
3001
2998
|
return {
|
|
3002
2999
|
content: [
|
|
3003
3000
|
{
|
|
3004
3001
|
type: "text",
|
|
3005
|
-
text: `
|
|
3002
|
+
text: `Task created: "${result.title}" (${result.taskId})`
|
|
3006
3003
|
}
|
|
3007
3004
|
]
|
|
3008
3005
|
};
|
|
@@ -3025,22 +3022,42 @@ function createWorkerTools(client, options) {
|
|
|
3025
3022
|
return errorResult(err);
|
|
3026
3023
|
}
|
|
3027
3024
|
}),
|
|
3028
|
-
createTool(WORKER_TOOLS.
|
|
3025
|
+
createTool(WORKER_TOOLS.UPDATE_TASK_STATUS, async (params) => {
|
|
3029
3026
|
try {
|
|
3030
3027
|
const taskId = validateStringParam(params, "taskId", true);
|
|
3031
|
-
const
|
|
3032
|
-
|
|
3028
|
+
const status2 = validateEnumParam(
|
|
3029
|
+
params,
|
|
3030
|
+
"status",
|
|
3031
|
+
["open", "done"],
|
|
3032
|
+
false
|
|
3033
|
+
);
|
|
3034
|
+
const awaiting = validateEnumParam(
|
|
3035
|
+
params,
|
|
3036
|
+
"awaiting",
|
|
3037
|
+
["bot", "human"],
|
|
3038
|
+
false
|
|
3039
|
+
);
|
|
3040
|
+
const reason = validateStringParam(params, "reason", false);
|
|
3041
|
+
const result = await client.updateTaskStatus({
|
|
3033
3042
|
taskId,
|
|
3034
|
-
|
|
3043
|
+
status: status2,
|
|
3044
|
+
awaiting,
|
|
3045
|
+
reason
|
|
3035
3046
|
});
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3047
|
+
if (status2 === "done") {
|
|
3048
|
+
try {
|
|
3049
|
+
options?.onTaskCompleted?.(taskId);
|
|
3050
|
+
} catch {
|
|
3051
|
+
}
|
|
3039
3052
|
}
|
|
3053
|
+
const details = [];
|
|
3054
|
+
if (status2) details.push(`Status: ${status2}`);
|
|
3055
|
+
if (awaiting) details.push(`Awaiting: ${awaiting}`);
|
|
3056
|
+
if (reason) details.push(`Reason: ${reason}`);
|
|
3040
3057
|
return formatTaskResponse(
|
|
3041
3058
|
result,
|
|
3042
|
-
`Task "${result.title}"
|
|
3043
|
-
|
|
3059
|
+
`Task "${result.title}" updated`,
|
|
3060
|
+
details
|
|
3044
3061
|
);
|
|
3045
3062
|
} catch (err) {
|
|
3046
3063
|
return errorResult(err);
|
|
@@ -3099,7 +3116,6 @@ Human attention is finite. You have unlimited stamina \u2014 they don't. Optimiz
|
|
|
3099
3116
|
|
|
3100
3117
|
- **Don't pile on.** If the board already has 3+ open tasks, think twice before proposing more. Help finish and clear existing work before adding new items.
|
|
3101
3118
|
- **Incremental over monolithic.** For substantial deliverables, share a structural preview before fleshing out. A quick "here's the outline \u2014 does this direction work?" saves everyone time versus a finished wall of text to review.
|
|
3102
|
-
- **Separate FYI from action needed.** Never make the human triage what needs their input. \`notify\` = no action needed. \`ask\` = needs their input. Be precise about which you're sending.
|
|
3103
3119
|
- **Fewer, better decisions.** Don't present 5 options when you can recommend 1 with reasoning. Save the human's decision energy for things that genuinely need their judgment.
|
|
3104
3120
|
- **Prefer simple output.** A focused 500-word draft beats a comprehensive 2000-word one the human has to pare down. Don't add sections, caveats, or "bonus" content unless asked.`;
|
|
3105
3121
|
}
|
|
@@ -3108,26 +3124,26 @@ function buildAgentDirective(ctx) {
|
|
|
3108
3124
|
|
|
3109
3125
|
## How You Work
|
|
3110
3126
|
|
|
3111
|
-
**Main thread = short and snappy.** Keep responses to 1-3 sentences. Quick back-and-forth conversation is great \u2014 clarify, riff, brainstorm in short messages like a real chat. But if something needs deep research, multiple rounds of clarification, or a deliverable \u2014
|
|
3127
|
+
**Main thread = short and snappy.** Keep responses to 1-3 sentences. Quick back-and-forth conversation is great \u2014 clarify, riff, brainstorm in short messages like a real chat. But if something needs deep research, multiple rounds of clarification, or a deliverable \u2014 create a task and move the work to a thread.
|
|
3112
3128
|
|
|
3113
3129
|
**The core loop:**
|
|
3114
3130
|
|
|
3115
3131
|
1. **Check state** \u2014 use \`deskfree_state\` to see tasks and files. Use \`deskfree_orient\` to recall relevant memories.
|
|
3116
|
-
2. **
|
|
3117
|
-
3. **Start work** \u2014 use \`deskfree_dispatch_worker\` with the taskId
|
|
3132
|
+
2. **Create tasks** \u2014 use \`deskfree_create_task\` to turn requests into concrete tasks.
|
|
3133
|
+
3. **Start work** \u2014 use \`deskfree_dispatch_worker\` with the taskId to work on the task in a thread.
|
|
3118
3134
|
4. **Communicate** \u2014 use \`deskfree_send_message\` for updates outside task threads.
|
|
3119
3135
|
|
|
3120
|
-
**Before
|
|
3121
|
-
- **One-off task** ("proofread this") \u2014
|
|
3122
|
-
- **New aspiration** ("I want to start posting on LinkedIn") \u2014 don't rush to
|
|
3123
|
-
- Never call \`
|
|
3136
|
+
**Before creating a task, qualify the request.** Figure out what kind of thing this is:
|
|
3137
|
+
- **One-off task** ("proofread this") \u2014 create a task directly.
|
|
3138
|
+
- **New aspiration** ("I want to start posting on LinkedIn") \u2014 don't rush to create a task. Ask 1-2 short qualifying questions to understand the real goal.
|
|
3139
|
+
- Never call \`deskfree_create_task\` as your very first action \u2014 qualify first, even if briefly.
|
|
3124
3140
|
|
|
3125
3141
|
**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.
|
|
3126
3142
|
|
|
3127
|
-
In the main thread you
|
|
3143
|
+
In the main thread you create tasks and coordinate \u2014 the actual work happens in task threads. Use \`deskfree_dispatch_worker\` to start working on tasks.
|
|
3128
3144
|
- When a human writes in a task thread, decide:
|
|
3129
3145
|
- **Continuation of the same task?** \u2192 reopen and pick it back up.
|
|
3130
|
-
- **New/different work request?** \u2192
|
|
3146
|
+
- **New/different work request?** \u2192 create it as a new task (don't reopen the old one).
|
|
3131
3147
|
- **Just confirmation or deferred?** \u2192 leave it for now.
|
|
3132
3148
|
- Estimate token cost per task \u2014 consider files to read, reasoning, output.
|
|
3133
3149
|
|
|
@@ -3151,7 +3167,7 @@ function buildWorkerDirective(ctx) {
|
|
|
3151
3167
|
## You're In a Task Thread
|
|
3152
3168
|
You're the same ${ctx.botName} from the main thread, now focused on a specific task. Same voice, same personality \u2014 just heads-down on the work.
|
|
3153
3169
|
|
|
3154
|
-
Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient,
|
|
3170
|
+
Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_update_task_status, deskfree_send_message, deskfree_create_task.
|
|
3155
3171
|
|
|
3156
3172
|
**Context loading:**
|
|
3157
3173
|
- If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
|
|
@@ -3162,24 +3178,30 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
|
|
|
3162
3178
|
- If no pre-loaded context (edge case/fallback), call \`deskfree_start_task\` with your taskId to load it.
|
|
3163
3179
|
- If continuing from a previous conversation (you can see prior tool calls and context), respond directly to the human's latest message \u2014 do NOT call deskfree_start_task again.
|
|
3164
3180
|
|
|
3181
|
+
**Your text responses are automatically streamed to the human as messages.** You do NOT need to call \`deskfree_send_message\` to talk \u2014 just write your response text directly. Only use \`deskfree_send_message\` when you need to send a message mid-tool-execution (e.g. a progress update while doing file operations).
|
|
3182
|
+
|
|
3165
3183
|
**Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
|
|
3166
3184
|
|
|
3167
3185
|
1. **Orient** \u2014 Your first message includes operating memory and task-relevant memories. Read any relevant files with \`deskfree_read_file\`. If you need more context mid-task, use \`deskfree_orient\` with a specific query to recall relevant memories.
|
|
3168
|
-
2. **Align** \u2014
|
|
3169
|
-
- **Judgment calls or creative direction?** State your assumptions and approach,
|
|
3170
|
-
- **Straightforward execution?** Proceed immediately
|
|
3186
|
+
2. **Align** \u2014 State briefly what you found and what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.") Just write this as your response text \u2014 do NOT use \`deskfree_send_message\`.
|
|
3187
|
+
- **Judgment calls or creative direction?** State your assumptions and approach, then set \`awaiting: 'human'\` via \`deskfree_update_task_status\` and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
|
|
3188
|
+
- **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
|
|
3171
3189
|
3. **Work** \u2014 Execute the task. Update existing files with \`deskfree_update_file\` or create new ones with \`deskfree_create_file\`. Pass your taskId so updates appear in the thread. For large deliverables, build incrementally \u2014 share structure/outline first, then flesh out. Don't produce a finished 2000-word document and ask for review in one shot.
|
|
3172
|
-
4. **Deliver** \u2014
|
|
3190
|
+
4. **Deliver** \u2014 When work is ready for review, set \`awaiting: 'human'\` via \`deskfree_update_task_status\`. The human will complete the task when satisfied.
|
|
3173
3191
|
|
|
3174
3192
|
**Push back when warranted:**
|
|
3175
3193
|
- If task instructions seem unclear, contradictory, or misguided \u2014 say so. "This task asks for X, but based on [context], Y might work better because..." is more useful than silently executing a flawed plan.
|
|
3176
|
-
- If you hit genuine ambiguity mid-task, send
|
|
3194
|
+
- If you hit genuine ambiguity mid-task, send a message and set \`awaiting: 'human'\`. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
|
|
3177
3195
|
- You're a teammate, not a task executor. Have an opinion when you have the context to form one.
|
|
3178
3196
|
|
|
3197
|
+
**New requests mid-task:**
|
|
3198
|
+
- When the human asks for something new in your thread, don't make it awkward \u2014 just roll with it. Qualify naturally ("Sure \u2014 what kind of audit?"), get enough clarity to write a well-scoped task, then use \`deskfree_create_task\`. No meta-commentary about scope boundaries or "that's outside this task." Just be a good conversationalist who happens to spin up a new task.
|
|
3199
|
+
- Every task needs a concrete deliverable and bounded scope. "Audit the codebase" is not a task \u2014 "Review tRPC error handling and list inconsistencies" is. If the human's request is too vague, ask a narrowing question before creating a task.
|
|
3200
|
+
- If you notice follow-up work yourself while executing, create a task immediately \u2014 don't wait until completion.
|
|
3201
|
+
|
|
3179
3202
|
**File rules:**
|
|
3180
3203
|
- Create files when your task naturally produces them. Don't be afraid to create multiple files if the work calls for it.
|
|
3181
3204
|
- Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
|
|
3182
|
-
- If you discover work that falls outside your task's scope, use \`deskfree_propose\` to suggest follow-up tasks immediately \u2014 don't wait until completion. Propose as you discover, then stay focused on your current task.
|
|
3183
3205
|
|
|
3184
3206
|
**Learnings \u2014 record aggressively:**
|
|
3185
3207
|
Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
|
|
@@ -3217,7 +3239,7 @@ On each heartbeat, run through this checklist:
|
|
|
3217
3239
|
|
|
3218
3240
|
### 1. Work the queue
|
|
3219
3241
|
- Run \`deskfree_state\` to get the full workspace snapshot.
|
|
3220
|
-
- **Check board load.** If there are 3+ tasks awaiting human review or input, skip proactive
|
|
3242
|
+
- **Check board load.** If there are 3+ tasks awaiting human review or input, skip proactive task creation entirely \u2014 the human has enough on their plate. Focus only on dispatching approved work.
|
|
3221
3243
|
- Any open tasks with awaiting=bot? Use \`deskfree_dispatch_worker\` to start working on each one. Pass the taskId.
|
|
3222
3244
|
- Any open tasks that seem stalled (no recent activity)? Check on them.
|
|
3223
3245
|
|
|
@@ -3232,7 +3254,7 @@ After handling the queue, step back and think about the bigger picture. You have
|
|
|
3232
3254
|
|
|
3233
3255
|
**Then act \u2014 but only if you have something genuinely useful:**
|
|
3234
3256
|
|
|
3235
|
-
*Things you can do* \u2014 research, drafts, analysis, prep.
|
|
3257
|
+
*Things you can do* \u2014 research, drafts, analysis, prep. Create a task via \`deskfree_create_task\`. One focused task, not a batch.
|
|
3236
3258
|
|
|
3237
3259
|
*Things the human should do* \u2014 nudges, reminders, conversation starters. Send via \`deskfree_send_message\`. Keep it brief and genuinely helpful, not nagging.
|
|
3238
3260
|
|
|
@@ -3248,7 +3270,7 @@ function buildSleepDirective(ctx) {
|
|
|
3248
3270
|
## Nightly Sleep Cycle \u2014 Memory Consolidation
|
|
3249
3271
|
You're running your nightly cycle to consolidate observations into long-term memory.
|
|
3250
3272
|
|
|
3251
|
-
Tools available: deskfree_state,
|
|
3273
|
+
Tools available: deskfree_state, deskfree_create_task, deskfree_send_message, deskfree_learning.
|
|
3252
3274
|
|
|
3253
3275
|
Your job: classify new observations, decide merges/rewrites of existing entries, and write the operating memory summary. Code handles strength scoring, decay, archival, and dedup \u2014 you do semantics.
|
|
3254
3276
|
|
|
@@ -3335,16 +3357,16 @@ Write a ~1500 token markdown summary with these sections:
|
|
|
3335
3357
|
After outputting the consolidation result:
|
|
3336
3358
|
1. Call \`deskfree_state\` to see the board.
|
|
3337
3359
|
2. Send a brief main-thread message via \`deskfree_send_message\` summarizing what was consolidated (1-2 sentences).
|
|
3338
|
-
3. Check for recurring commitments in operating memory \u2014
|
|
3339
|
-
4. One proactive
|
|
3360
|
+
3. Check for recurring commitments in operating memory \u2014 create via \`deskfree_create_task\` if needed.
|
|
3361
|
+
4. One proactive task max. Skip if nothing merits it or board is busy (3+ items needing human attention).`;
|
|
3340
3362
|
}
|
|
3341
3363
|
function buildDuskDirective(ctx) {
|
|
3342
3364
|
return `${identityBlock(ctx)}
|
|
3343
3365
|
|
|
3344
3366
|
## Evening Dusk Cycle
|
|
3345
|
-
You're running your evening cycle to review the day,
|
|
3367
|
+
You're running your evening cycle to review the day, create overnight tasks, and brief the human.
|
|
3346
3368
|
|
|
3347
|
-
Tools available: deskfree_state,
|
|
3369
|
+
Tools available: deskfree_state, deskfree_create_task, deskfree_send_message, deskfree_read_file, deskfree_orient.
|
|
3348
3370
|
|
|
3349
3371
|
---
|
|
3350
3372
|
|
|
@@ -3375,28 +3397,28 @@ Think about work that can be done autonomously overnight \u2014 WITHOUT human ju
|
|
|
3375
3397
|
- Creative work where the human has strong opinions on direction
|
|
3376
3398
|
- Anything the human explicitly said to wait on
|
|
3377
3399
|
|
|
3378
|
-
### 3.
|
|
3400
|
+
### 3. CREATE TASKS
|
|
3379
3401
|
|
|
3380
3402
|
If you identified useful overnight work:
|
|
3381
|
-
1. **Check board load first.** Count open and awaiting-review tasks. If the human already has 3+ items needing their attention, limit to 1
|
|
3382
|
-
2. Use \`
|
|
3403
|
+
1. **Check board load first.** Count open and awaiting-review tasks. If the human already has 3+ items needing their attention, limit to 1 task max \u2014 or skip entirely. Don't pile on.
|
|
3404
|
+
2. Use \`deskfree_create_task\` to create 1-3 well-scoped tasks. Quality over quantity.
|
|
3383
3405
|
3. Each task should be self-contained \u2014 it must be completable without human input.
|
|
3384
3406
|
4. Set \`scheduledFor\` if work should start at a specific time (e.g. early morning).
|
|
3385
|
-
5. If nothing genuinely useful can be done overnight, skip
|
|
3407
|
+
5. If nothing genuinely useful can be done overnight, skip task creation entirely. Don't force it.
|
|
3386
3408
|
|
|
3387
3409
|
### 4. BRIEF THE HUMAN
|
|
3388
3410
|
|
|
3389
3411
|
Send a brief main-thread message via \`deskfree_send_message\`:
|
|
3390
3412
|
- 2-3 sentence summary: what happened today + what you're proposing for overnight (if anything).
|
|
3391
3413
|
- Keep it conversational and useful \u2014 this is the human's "end of day" touchpoint.
|
|
3392
|
-
- If no
|
|
3414
|
+
- If no tasks created, still send a brief day summary.
|
|
3393
3415
|
|
|
3394
3416
|
### Rules
|
|
3395
|
-
- Do NOT
|
|
3417
|
+
- Do NOT create tasks for things already on the board or recently completed.
|
|
3396
3418
|
- Do NOT repeat suggestions the human previously ignored or rejected.
|
|
3397
3419
|
- Quality over quantity. One good task beats three mediocre ones.
|
|
3398
3420
|
- Keep the briefing message short and actionable.
|
|
3399
|
-
- Cross-reference memory for recurring patterns \u2014 if something is due,
|
|
3421
|
+
- Cross-reference memory for recurring patterns \u2014 if something is due, create a task for it.
|
|
3400
3422
|
- Use \`deskfree_read_file\` only if you need file content beyond what's in your prompt.`;
|
|
3401
3423
|
}
|
|
3402
3424
|
function setActiveWs(ws) {
|
|
@@ -3540,31 +3562,25 @@ function validateField(opts) {
|
|
|
3540
3562
|
return patternMessage ?? `${name2} contains invalid characters`;
|
|
3541
3563
|
return null;
|
|
3542
3564
|
}
|
|
3543
|
-
function isLocalDevelopmentHost(
|
|
3544
|
-
return
|
|
3565
|
+
function isLocalDevelopmentHost(hostname2) {
|
|
3566
|
+
return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2.endsWith(".local") || hostname2.endsWith(".localhost") || /^192\.168\./.test(hostname2) || /^10\./.test(hostname2) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname2);
|
|
3545
3567
|
}
|
|
3546
|
-
function
|
|
3547
|
-
const fieldError = validateField({ value, name: "Bot
|
|
3568
|
+
function validateBotId(value) {
|
|
3569
|
+
const fieldError = validateField({ value, name: "Bot ID" });
|
|
3548
3570
|
if (fieldError) return fieldError;
|
|
3549
3571
|
const trimmed = value.trim();
|
|
3550
|
-
if (
|
|
3551
|
-
return
|
|
3572
|
+
if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
|
|
3573
|
+
return "Bot ID contains whitespace characters. Please copy it exactly as shown in DeskFree.";
|
|
3552
3574
|
}
|
|
3553
3575
|
const patternError = validateField({
|
|
3554
3576
|
value,
|
|
3555
|
-
name: "Bot
|
|
3577
|
+
name: "Bot ID",
|
|
3556
3578
|
minLength: 10,
|
|
3557
|
-
maxLength:
|
|
3558
|
-
pattern: /^
|
|
3559
|
-
patternMessage:
|
|
3579
|
+
maxLength: 14,
|
|
3580
|
+
pattern: /^[A-Z][A-Z0-9]+$/,
|
|
3581
|
+
patternMessage: "Bot ID contains invalid characters. Only uppercase letters and numbers are allowed."
|
|
3560
3582
|
});
|
|
3561
3583
|
if (patternError) return patternError;
|
|
3562
|
-
if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
|
|
3563
|
-
return "Bot token contains whitespace characters. Please copy the token exactly as shown in DeskFree.";
|
|
3564
|
-
}
|
|
3565
|
-
if (trimmed === "bot_your_token_here" || trimmed === "bot_example") {
|
|
3566
|
-
return "Please replace the placeholder with your actual bot token from DeskFree";
|
|
3567
|
-
}
|
|
3568
3584
|
return null;
|
|
3569
3585
|
}
|
|
3570
3586
|
function validateUrl(value, name2, allowedProtocols, protocolError) {
|
|
@@ -7351,13 +7367,13 @@ var init_dist = __esm({
|
|
|
7351
7367
|
}
|
|
7352
7368
|
};
|
|
7353
7369
|
DeskFreeClient = class {
|
|
7354
|
-
|
|
7370
|
+
getToken;
|
|
7355
7371
|
apiUrl;
|
|
7356
7372
|
requestTimeoutMs;
|
|
7357
|
-
constructor(
|
|
7358
|
-
this.
|
|
7359
|
-
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
7360
|
-
this.requestTimeoutMs = options
|
|
7373
|
+
constructor(options) {
|
|
7374
|
+
this.getToken = options.getToken;
|
|
7375
|
+
this.apiUrl = options.apiUrl.replace(/\/$/, "");
|
|
7376
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
7361
7377
|
}
|
|
7362
7378
|
async request(method, procedure, input) {
|
|
7363
7379
|
const url = method === "GET" && input ? `${this.apiUrl}/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}` : `${this.apiUrl}/${procedure}`;
|
|
@@ -7367,7 +7383,7 @@ var init_dist = __esm({
|
|
|
7367
7383
|
const response = await fetch(url, {
|
|
7368
7384
|
method,
|
|
7369
7385
|
headers: {
|
|
7370
|
-
Authorization: `
|
|
7386
|
+
Authorization: `Bearer ${this.getToken()}`,
|
|
7371
7387
|
"Content-Type": "application/json"
|
|
7372
7388
|
},
|
|
7373
7389
|
body: method === "POST" ? JSON.stringify(input) : void 0,
|
|
@@ -7498,15 +7514,10 @@ var init_dist = __esm({
|
|
|
7498
7514
|
this.requireNonEmpty(input.taskId, "taskId");
|
|
7499
7515
|
return this.request("POST", "tasks.reportUsage", input);
|
|
7500
7516
|
}
|
|
7501
|
-
/**
|
|
7502
|
-
async
|
|
7503
|
-
this.requireNonEmpty(input.taskId, "taskId");
|
|
7504
|
-
return this.request("POST", "tasks.complete", input);
|
|
7505
|
-
}
|
|
7506
|
-
/** Reopen a completed/human task back to bot status for further work. */
|
|
7507
|
-
async reopenTask(input) {
|
|
7517
|
+
/** Update task status and/or awaiting state. */
|
|
7518
|
+
async updateTaskStatus(input) {
|
|
7508
7519
|
this.requireNonEmpty(input.taskId, "taskId");
|
|
7509
|
-
return this.request("POST", "tasks.
|
|
7520
|
+
return this.request("POST", "tasks.updateStatus", input);
|
|
7510
7521
|
}
|
|
7511
7522
|
/** Snooze a task until a specified time. Task is hidden from active queues until then. */
|
|
7512
7523
|
async snoozeTask(input) {
|
|
@@ -7545,17 +7556,9 @@ var init_dist = __esm({
|
|
|
7545
7556
|
async consolidateMemory(input) {
|
|
7546
7557
|
return this.request("POST", "memory.consolidate", input);
|
|
7547
7558
|
}
|
|
7548
|
-
/**
|
|
7549
|
-
async
|
|
7550
|
-
|
|
7551
|
-
throw new DeskFreeError(
|
|
7552
|
-
"client",
|
|
7553
|
-
"tasks",
|
|
7554
|
-
"tasks array is required and cannot be empty",
|
|
7555
|
-
"Missing required parameter: tasks."
|
|
7556
|
-
);
|
|
7557
|
-
}
|
|
7558
|
-
return this.request("POST", "tasks.propose", input);
|
|
7559
|
+
/** Create a task directly — no approval needed. */
|
|
7560
|
+
async createTask(input) {
|
|
7561
|
+
return this.request("POST", "tasks.create", input);
|
|
7559
7562
|
}
|
|
7560
7563
|
/**
|
|
7561
7564
|
* Fetch runtime bootstrap config from the backend.
|
|
@@ -7580,7 +7583,7 @@ var init_dist = __esm({
|
|
|
7580
7583
|
{
|
|
7581
7584
|
method: "GET",
|
|
7582
7585
|
headers: {
|
|
7583
|
-
Authorization: `
|
|
7586
|
+
Authorization: `Bearer ${this.getToken()}`,
|
|
7584
7587
|
"Content-Type": "application/json"
|
|
7585
7588
|
},
|
|
7586
7589
|
signal: controller.signal
|
|
@@ -7922,14 +7925,24 @@ var init_dist = __esm({
|
|
|
7922
7925
|
})
|
|
7923
7926
|
})
|
|
7924
7927
|
},
|
|
7925
|
-
|
|
7926
|
-
name: "
|
|
7927
|
-
description: "
|
|
7928
|
+
UPDATE_TASK_STATUS: {
|
|
7929
|
+
name: "deskfree_update_task_status",
|
|
7930
|
+
description: "Update a task's status and/or awaiting state. Use to reopen done tasks, mark tasks done, or change who the task is awaiting.",
|
|
7928
7931
|
parameters: Type.Object({
|
|
7929
|
-
taskId: Type.String({ description: "Task UUID to
|
|
7932
|
+
taskId: Type.String({ description: "Task UUID to update" }),
|
|
7933
|
+
status: Type.Optional(
|
|
7934
|
+
Type.Union([Type.Literal("open"), Type.Literal("done")], {
|
|
7935
|
+
description: "New status (open or done)"
|
|
7936
|
+
})
|
|
7937
|
+
),
|
|
7938
|
+
awaiting: Type.Optional(
|
|
7939
|
+
Type.Union([Type.Literal("bot"), Type.Literal("human")], {
|
|
7940
|
+
description: "Who the task is awaiting (bot or human). Set to human when work is ready for review."
|
|
7941
|
+
})
|
|
7942
|
+
),
|
|
7930
7943
|
reason: Type.Optional(
|
|
7931
7944
|
Type.String({
|
|
7932
|
-
description: "
|
|
7945
|
+
description: "Brief explanation (shown in task thread as system message)"
|
|
7933
7946
|
})
|
|
7934
7947
|
)
|
|
7935
7948
|
})
|
|
@@ -7938,9 +7951,6 @@ var init_dist = __esm({
|
|
|
7938
7951
|
name: "deskfree_send_message",
|
|
7939
7952
|
description: "Send a message to the human. Keep it short \u2014 1-3 sentences. No walls of text.",
|
|
7940
7953
|
parameters: Type.Object({
|
|
7941
|
-
type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
|
|
7942
|
-
description: "notify = general update (quiet). ask = needs human attention (surfaces prominently)."
|
|
7943
|
-
}),
|
|
7944
7954
|
content: Type.String({
|
|
7945
7955
|
description: "Message content."
|
|
7946
7956
|
}),
|
|
@@ -7951,63 +7961,49 @@ var init_dist = __esm({
|
|
|
7951
7961
|
)
|
|
7952
7962
|
})
|
|
7953
7963
|
},
|
|
7954
|
-
|
|
7955
|
-
name: "
|
|
7956
|
-
description: "
|
|
7964
|
+
CREATE_TASK: {
|
|
7965
|
+
name: "deskfree_create_task",
|
|
7966
|
+
description: "Create a task directly. The task is created immediately and available for work. Put all details in the instructions field using simple markdown (bold, lists, inline code). Do NOT use markdown headers (#) \u2014 use **bold text** instead for section labels.",
|
|
7957
7967
|
parameters: Type.Object({
|
|
7958
|
-
|
|
7968
|
+
title: Type.String({
|
|
7969
|
+
description: "Task title \u2014 short, action-oriented (max 200 chars)"
|
|
7970
|
+
}),
|
|
7971
|
+
instructions: Type.Optional(
|
|
7959
7972
|
Type.String({
|
|
7960
|
-
description: "
|
|
7973
|
+
description: "Detailed instructions, constraints, or context for the task."
|
|
7961
7974
|
})
|
|
7962
7975
|
),
|
|
7963
|
-
|
|
7964
|
-
Type.
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
}),
|
|
7968
|
-
instructions: Type.Optional(
|
|
7969
|
-
Type.String({
|
|
7970
|
-
description: "Detailed instructions, constraints, or context for the task."
|
|
7971
|
-
})
|
|
7972
|
-
),
|
|
7973
|
-
estimatedTokens: Type.Optional(
|
|
7974
|
-
Type.Number({
|
|
7975
|
-
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7976
|
-
})
|
|
7977
|
-
),
|
|
7978
|
-
scheduledFor: Type.Optional(
|
|
7979
|
-
Type.String({
|
|
7980
|
-
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7981
|
-
})
|
|
7982
|
-
),
|
|
7983
|
-
inputFileIds: Type.Optional(
|
|
7984
|
-
Type.Array(
|
|
7985
|
-
Type.String({ description: "File ID to pre-load as context" }),
|
|
7986
|
-
{
|
|
7987
|
-
description: "File IDs to read as context input for this task",
|
|
7988
|
-
maxItems: 20
|
|
7989
|
-
}
|
|
7990
|
-
)
|
|
7991
|
-
),
|
|
7992
|
-
outputFileIds: Type.Optional(
|
|
7993
|
-
Type.Array(
|
|
7994
|
-
Type.String({ description: "File ID to update as deliverable" }),
|
|
7995
|
-
{
|
|
7996
|
-
description: "File IDs this task will produce or update",
|
|
7997
|
-
maxItems: 10
|
|
7998
|
-
}
|
|
7999
|
-
)
|
|
8000
|
-
)
|
|
8001
|
-
}),
|
|
8002
|
-
{
|
|
8003
|
-
description: "Array of tasks to propose (1-20)",
|
|
8004
|
-
minItems: 1,
|
|
8005
|
-
maxItems: 20
|
|
8006
|
-
}
|
|
7976
|
+
estimatedTokens: Type.Optional(
|
|
7977
|
+
Type.Number({
|
|
7978
|
+
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7979
|
+
})
|
|
8007
7980
|
),
|
|
8008
|
-
|
|
7981
|
+
scheduledFor: Type.Optional(
|
|
7982
|
+
Type.String({
|
|
7983
|
+
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7984
|
+
})
|
|
7985
|
+
),
|
|
7986
|
+
inputFileIds: Type.Optional(
|
|
7987
|
+
Type.Array(
|
|
7988
|
+
Type.String({ description: "File ID to pre-load as context" }),
|
|
7989
|
+
{
|
|
7990
|
+
description: "File IDs to read as context input for this task",
|
|
7991
|
+
maxItems: 20
|
|
7992
|
+
}
|
|
7993
|
+
)
|
|
7994
|
+
),
|
|
7995
|
+
outputFileIds: Type.Optional(
|
|
7996
|
+
Type.Array(
|
|
7997
|
+
Type.String({ description: "File ID to update as deliverable" }),
|
|
7998
|
+
{
|
|
7999
|
+
description: "File IDs this task will produce or update",
|
|
8000
|
+
maxItems: 10
|
|
8001
|
+
}
|
|
8002
|
+
)
|
|
8003
|
+
),
|
|
8004
|
+
suggestedByTaskId: Type.Optional(
|
|
8009
8005
|
Type.String({
|
|
8010
|
-
description: "
|
|
8006
|
+
description: "Parent task ID (for follow-up tasks created from within a task thread)"
|
|
8011
8007
|
})
|
|
8012
8008
|
)
|
|
8013
8009
|
})
|
|
@@ -8072,23 +8068,10 @@ var init_dist = __esm({
|
|
|
8072
8068
|
)
|
|
8073
8069
|
})
|
|
8074
8070
|
},
|
|
8075
|
-
COMPLETE_TASK: {
|
|
8076
|
-
name: "deskfree_complete_task",
|
|
8077
|
-
description: "Mark a task as done. Only call when truly finished and human confirmed.",
|
|
8078
|
-
parameters: Type.Object({
|
|
8079
|
-
taskId: Type.String({ description: "Task UUID" }),
|
|
8080
|
-
humanApproved: Type.Boolean({
|
|
8081
|
-
description: "Must be true. Confirms the human reviewed and approved completion. Backend validates a human message exists after your last ask."
|
|
8082
|
-
})
|
|
8083
|
-
})
|
|
8084
|
-
},
|
|
8085
8071
|
SEND_MESSAGE: {
|
|
8086
8072
|
name: "deskfree_send_message",
|
|
8087
8073
|
description: "Send a message in the task thread. Keep it short \u2014 1-3 sentences.",
|
|
8088
8074
|
parameters: Type.Object({
|
|
8089
|
-
type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
|
|
8090
|
-
description: "notify = progress update (quiet, collapsible). ask = needs human attention (surfaces to main thread). Terminate after sending an ask."
|
|
8091
|
-
}),
|
|
8092
8075
|
content: Type.String({
|
|
8093
8076
|
description: "Message content."
|
|
8094
8077
|
}),
|
|
@@ -8099,63 +8082,49 @@ var init_dist = __esm({
|
|
|
8099
8082
|
)
|
|
8100
8083
|
})
|
|
8101
8084
|
},
|
|
8102
|
-
|
|
8103
|
-
name: "
|
|
8104
|
-
description: "
|
|
8085
|
+
CREATE_TASK: {
|
|
8086
|
+
name: "deskfree_create_task",
|
|
8087
|
+
description: "Create a task directly. The task is created immediately and available for work. Put all details in the instructions field using simple markdown (bold, lists, inline code). Do NOT use markdown headers (#) \u2014 use **bold text** instead for section labels.",
|
|
8105
8088
|
parameters: Type.Object({
|
|
8106
|
-
|
|
8089
|
+
title: Type.String({
|
|
8090
|
+
description: "Task title \u2014 short, action-oriented (max 200 chars)"
|
|
8091
|
+
}),
|
|
8092
|
+
instructions: Type.Optional(
|
|
8107
8093
|
Type.String({
|
|
8108
|
-
description: "
|
|
8094
|
+
description: "Detailed instructions, constraints, or context for the task."
|
|
8109
8095
|
})
|
|
8110
8096
|
),
|
|
8111
|
-
|
|
8112
|
-
Type.
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
}),
|
|
8116
|
-
instructions: Type.Optional(
|
|
8117
|
-
Type.String({
|
|
8118
|
-
description: "Detailed instructions, constraints, or context for the task."
|
|
8119
|
-
})
|
|
8120
|
-
),
|
|
8121
|
-
estimatedTokens: Type.Optional(
|
|
8122
|
-
Type.Number({
|
|
8123
|
-
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
8124
|
-
})
|
|
8125
|
-
),
|
|
8126
|
-
scheduledFor: Type.Optional(
|
|
8127
|
-
Type.String({
|
|
8128
|
-
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
8129
|
-
})
|
|
8130
|
-
),
|
|
8131
|
-
inputFileIds: Type.Optional(
|
|
8132
|
-
Type.Array(
|
|
8133
|
-
Type.String({ description: "File ID to pre-load as context" }),
|
|
8134
|
-
{
|
|
8135
|
-
description: "File IDs to read as context input for this task",
|
|
8136
|
-
maxItems: 20
|
|
8137
|
-
}
|
|
8138
|
-
)
|
|
8139
|
-
),
|
|
8140
|
-
outputFileIds: Type.Optional(
|
|
8141
|
-
Type.Array(
|
|
8142
|
-
Type.String({ description: "File ID to update as deliverable" }),
|
|
8143
|
-
{
|
|
8144
|
-
description: "File IDs this task will produce or update",
|
|
8145
|
-
maxItems: 10
|
|
8146
|
-
}
|
|
8147
|
-
)
|
|
8148
|
-
)
|
|
8149
|
-
}),
|
|
8150
|
-
{
|
|
8151
|
-
description: "Array of tasks to propose (1-20)",
|
|
8152
|
-
minItems: 1,
|
|
8153
|
-
maxItems: 20
|
|
8154
|
-
}
|
|
8097
|
+
estimatedTokens: Type.Optional(
|
|
8098
|
+
Type.Number({
|
|
8099
|
+
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
8100
|
+
})
|
|
8155
8101
|
),
|
|
8156
|
-
|
|
8102
|
+
scheduledFor: Type.Optional(
|
|
8103
|
+
Type.String({
|
|
8104
|
+
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
8105
|
+
})
|
|
8106
|
+
),
|
|
8107
|
+
inputFileIds: Type.Optional(
|
|
8108
|
+
Type.Array(
|
|
8109
|
+
Type.String({ description: "File ID to pre-load as context" }),
|
|
8110
|
+
{
|
|
8111
|
+
description: "File IDs to read as context input for this task",
|
|
8112
|
+
maxItems: 20
|
|
8113
|
+
}
|
|
8114
|
+
)
|
|
8115
|
+
),
|
|
8116
|
+
outputFileIds: Type.Optional(
|
|
8117
|
+
Type.Array(
|
|
8118
|
+
Type.String({ description: "File ID to update as deliverable" }),
|
|
8119
|
+
{
|
|
8120
|
+
description: "File IDs this task will produce or update",
|
|
8121
|
+
maxItems: 10
|
|
8122
|
+
}
|
|
8123
|
+
)
|
|
8124
|
+
),
|
|
8125
|
+
suggestedByTaskId: Type.Optional(
|
|
8157
8126
|
Type.String({
|
|
8158
|
-
description: "
|
|
8127
|
+
description: "Parent task ID (for follow-up tasks created from within a task thread)"
|
|
8159
8128
|
})
|
|
8160
8129
|
)
|
|
8161
8130
|
})
|
|
@@ -8229,34 +8198,55 @@ var init_dist = __esm({
|
|
|
8229
8198
|
)
|
|
8230
8199
|
})
|
|
8231
8200
|
},
|
|
8232
|
-
|
|
8201
|
+
UPDATE_TASK_STATUS: {
|
|
8202
|
+
name: "deskfree_update_task_status",
|
|
8203
|
+
description: "Update a task's status and/or awaiting state. Use to mark tasks done, reopen them, or signal that you're awaiting human input.",
|
|
8204
|
+
parameters: Type.Object({
|
|
8205
|
+
taskId: Type.String({ description: "Task UUID to update" }),
|
|
8206
|
+
status: Type.Optional(
|
|
8207
|
+
Type.Union([Type.Literal("open"), Type.Literal("done")], {
|
|
8208
|
+
description: "New status (open or done)"
|
|
8209
|
+
})
|
|
8210
|
+
),
|
|
8211
|
+
awaiting: Type.Optional(
|
|
8212
|
+
Type.Union([Type.Literal("bot"), Type.Literal("human")], {
|
|
8213
|
+
description: "Who the task is awaiting. Set to human when work is ready for review."
|
|
8214
|
+
})
|
|
8215
|
+
),
|
|
8216
|
+
reason: Type.Optional(
|
|
8217
|
+
Type.String({
|
|
8218
|
+
description: "Brief explanation (shown in task thread)"
|
|
8219
|
+
})
|
|
8220
|
+
)
|
|
8221
|
+
})
|
|
8222
|
+
},
|
|
8233
8223
|
SEND_MESSAGE: SHARED_TOOLS.SEND_MESSAGE,
|
|
8234
|
-
|
|
8224
|
+
CREATE_TASK: SHARED_TOOLS.CREATE_TASK
|
|
8235
8225
|
};
|
|
8236
8226
|
MAX_FULL_MESSAGES = 15;
|
|
8237
8227
|
DESKFREE_AGENT_DIRECTIVE = `## DeskFree \u2014 Main Thread
|
|
8238
|
-
You handle the main conversation thread. Your job: turn human intent into
|
|
8228
|
+
You handle the main conversation thread. Your job: turn human intent into concrete tasks, then start working on them.
|
|
8239
8229
|
|
|
8240
|
-
**Main thread = short and snappy.** Keep responses to 1-3 sentences. Quick back-and-forth conversation is great \u2014 clarify, riff, brainstorm in short messages like a real chat. But if something needs deep research, multiple rounds of clarification, or a deliverable \u2014
|
|
8230
|
+
**Main thread = short and snappy.** Keep responses to 1-3 sentences. Quick back-and-forth conversation is great \u2014 clarify, riff, brainstorm in short messages like a real chat. But if something needs deep research, multiple rounds of clarification, or a deliverable \u2014 create a task and move the work to a thread.
|
|
8241
8231
|
|
|
8242
|
-
**The core loop
|
|
8232
|
+
**The core loop:**
|
|
8243
8233
|
|
|
8244
8234
|
1. **Check state** \u2192 \`deskfree_state\` \u2014 see tasks and files. Use \`deskfree_orient\` to recall relevant memories.
|
|
8245
|
-
2. **
|
|
8246
|
-
3. **Start work** \u2192 \`deskfree_dispatch_worker\` with the taskId
|
|
8235
|
+
2. **Create tasks** \u2192 \`deskfree_create_task\` \u2014 turn requests into concrete tasks.
|
|
8236
|
+
3. **Start work** \u2192 \`deskfree_dispatch_worker\` with the taskId to work on the task in a thread.
|
|
8247
8237
|
4. **Communicate** \u2192 \`deskfree_send_message\` for updates outside task threads.
|
|
8248
8238
|
|
|
8249
|
-
**Before
|
|
8250
|
-
- **One-off task** ("proofread this") \u2192
|
|
8251
|
-
- **New aspiration** ("I want to start posting on LinkedIn") \u2192 don't rush to
|
|
8252
|
-
- Never call \`
|
|
8239
|
+
**Before creating a task, qualify the request.** Figure out what kind of thing this is:
|
|
8240
|
+
- **One-off task** ("proofread this") \u2192 create a task directly.
|
|
8241
|
+
- **New aspiration** ("I want to start posting on LinkedIn") \u2192 don't rush to create a task. Ask 1-2 short qualifying questions to understand the real goal.
|
|
8242
|
+
- Never call \`deskfree_create_task\` as your very first action \u2014 qualify first, even if briefly.
|
|
8253
8243
|
|
|
8254
8244
|
**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.
|
|
8255
8245
|
|
|
8256
|
-
In the main thread you
|
|
8246
|
+
In the main thread you create tasks and coordinate \u2014 the actual work happens in task threads. Use \`deskfree_dispatch_worker\` to start working on tasks.
|
|
8257
8247
|
- When a human writes in a task thread, decide:
|
|
8258
8248
|
- **Continuation of the same task?** \u2192 reopen and pick it back up.
|
|
8259
|
-
- **New/different work request?** \u2192
|
|
8249
|
+
- **New/different work request?** \u2192 create it as a new task (don't reopen the old one).
|
|
8260
8250
|
- **Just confirmation or deferred?** \u2192 leave it for now.
|
|
8261
8251
|
- Estimate token cost per task \u2014 consider files to read, reasoning, output.
|
|
8262
8252
|
|
|
@@ -8276,7 +8266,7 @@ Record immediately when: the human corrects you, expresses a preference, shares
|
|
|
8276
8266
|
DESKFREE_WORKER_DIRECTIVE = `## DeskFree \u2014 Task Thread
|
|
8277
8267
|
You're in a task thread, focused on a specific piece of work. Same you as in the main thread \u2014 same voice, same personality.
|
|
8278
8268
|
|
|
8279
|
-
Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient,
|
|
8269
|
+
Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_update_task_status, deskfree_send_message, deskfree_create_task.
|
|
8280
8270
|
|
|
8281
8271
|
**Context loading:**
|
|
8282
8272
|
- If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
|
|
@@ -8287,24 +8277,30 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
|
|
|
8287
8277
|
- If no pre-loaded context (edge case/fallback), call \`deskfree_start_task\` with your taskId to load it.
|
|
8288
8278
|
- If continuing from a previous conversation (you can see prior tool calls and context), respond directly to the human's latest message \u2014 do NOT call deskfree_start_task again.
|
|
8289
8279
|
|
|
8280
|
+
**Your text responses are automatically streamed to the human as messages.** You do NOT need to call \`deskfree_send_message\` to talk \u2014 just write your response text directly. Only use \`deskfree_send_message\` when you need to send a message mid-tool-execution (e.g. a progress update while doing file operations).
|
|
8281
|
+
|
|
8290
8282
|
**Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
|
|
8291
8283
|
|
|
8292
8284
|
1. **Orient** \u2014 Your first message includes operating memory and task-relevant memories. Read any relevant files with \`deskfree_read_file\`. If you need more context mid-task, use \`deskfree_orient\` with a specific query to recall relevant memories.
|
|
8293
|
-
2. **Align** \u2014
|
|
8294
|
-
- **Judgment calls or creative direction?** State your assumptions and approach,
|
|
8295
|
-
- **Straightforward execution?** Proceed immediately
|
|
8285
|
+
2. **Align** \u2014 State briefly what you found and what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.") Just write this as your response text \u2014 do NOT use \`deskfree_send_message\`.
|
|
8286
|
+
- **Judgment calls or creative direction?** State your assumptions and approach, then set \`awaiting: 'human'\` via \`deskfree_update_task_status\` and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
|
|
8287
|
+
- **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
|
|
8296
8288
|
3. **Work** \u2014 Execute the task. Update existing files with \`deskfree_update_file\` or create new ones with \`deskfree_create_file\`. Pass your taskId so updates appear in the thread. For large deliverables, build incrementally \u2014 share structure/outline first, then flesh out. Don't produce a finished 2000-word document and ask for review in one shot.
|
|
8297
|
-
4. **Deliver** \u2014
|
|
8289
|
+
4. **Deliver** \u2014 When work is ready for review, set \`awaiting: 'human'\` via \`deskfree_update_task_status\`. The human will complete the task when satisfied.
|
|
8298
8290
|
|
|
8299
8291
|
**Push back when warranted:**
|
|
8300
8292
|
- If task instructions seem unclear, contradictory, or misguided \u2014 say so. "This task asks for X, but based on [context], Y might work better because..." is more useful than silently executing a flawed plan.
|
|
8301
|
-
- If you hit genuine ambiguity mid-task, send
|
|
8293
|
+
- If you hit genuine ambiguity mid-task, send a message and set \`awaiting: 'human'\`. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
|
|
8302
8294
|
- You're a teammate, not a task executor. Have an opinion when you have the context to form one.
|
|
8303
8295
|
|
|
8296
|
+
**New requests mid-task:**
|
|
8297
|
+
- When the human asks for something new in your thread, don't make it awkward \u2014 just roll with it. Qualify naturally ("Sure \u2014 what kind of audit?"), get enough clarity to write a well-scoped task, then use \`deskfree_create_task\`. No meta-commentary about scope boundaries or "that's outside this task." Just be a good conversationalist who happens to spin up a new task.
|
|
8298
|
+
- Every task needs a concrete deliverable and bounded scope. "Audit the codebase" is not a task \u2014 "Review tRPC error handling and list inconsistencies" is. If the human's request is too vague, ask a narrowing question before creating a task.
|
|
8299
|
+
- If you notice follow-up work yourself while executing, create a task immediately \u2014 don't wait until completion.
|
|
8300
|
+
|
|
8304
8301
|
**File rules:**
|
|
8305
8302
|
- Create files when your task naturally produces them. Don't be afraid to create multiple files if the work calls for it.
|
|
8306
8303
|
- Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
|
|
8307
|
-
- If you discover work that falls outside your task's scope, use \`deskfree_propose\` to suggest follow-up tasks immediately \u2014 don't wait until completion. Propose as you discover, then stay focused on your current task.
|
|
8308
8304
|
|
|
8309
8305
|
**Learnings \u2014 record aggressively:**
|
|
8310
8306
|
Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
|
|
@@ -8644,43 +8640,31 @@ var init_orchestrator = __esm({
|
|
|
8644
8640
|
];
|
|
8645
8641
|
}
|
|
8646
8642
|
});
|
|
8647
|
-
function
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8643
|
+
function getStageDomain(stage, domain) {
|
|
8644
|
+
if (domain.startsWith(`${stage}.`)) return domain;
|
|
8645
|
+
return `${stage}.${domain}`;
|
|
8646
|
+
}
|
|
8647
|
+
function deriveApiUrl() {
|
|
8648
|
+
const domain = process.env["DESKFREE_DOMAIN"] ?? "dev.deskfree.ai";
|
|
8649
|
+
const stage = process.env["STAGE"] ?? "dev";
|
|
8650
|
+
return `https://${getStageDomain(stage, domain)}/v1/bot`;
|
|
8651
|
+
}
|
|
8652
|
+
function deriveWsUrl() {
|
|
8653
|
+
const domain = process.env["DESKFREE_DOMAIN"] ?? "dev.deskfree.ai";
|
|
8654
|
+
return `wss://ws.${domain}`;
|
|
8653
8655
|
}
|
|
8654
8656
|
function loadConfig() {
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
const decoded = JSON.parse(
|
|
8661
|
-
Buffer.from(launch, "base64").toString("utf8")
|
|
8662
|
-
);
|
|
8663
|
-
botToken = decoded.botToken;
|
|
8664
|
-
apiUrl = decoded.apiUrl;
|
|
8665
|
-
if (!botToken || !apiUrl) {
|
|
8666
|
-
throw new Error(
|
|
8667
|
-
"Missing botToken or apiUrl in DESKFREE_LAUNCH payload"
|
|
8668
|
-
);
|
|
8669
|
-
}
|
|
8670
|
-
} catch (err) {
|
|
8671
|
-
if (err instanceof SyntaxError) {
|
|
8672
|
-
throw new Error("DESKFREE_LAUNCH is not valid base64-encoded JSON");
|
|
8673
|
-
}
|
|
8674
|
-
throw err;
|
|
8675
|
-
}
|
|
8676
|
-
} else {
|
|
8677
|
-
botToken = requireEnv("DESKFREE_BOT_TOKEN");
|
|
8678
|
-
apiUrl = requireEnv("DESKFREE_API_URL");
|
|
8657
|
+
const botId = process.env["BOT"] ?? process.env["DESKFREE_BOT_ID"];
|
|
8658
|
+
if (!botId) {
|
|
8659
|
+
throw new Error(
|
|
8660
|
+
"Missing bot ID. Set the BOT environment variable (e.g. BOT=BFRH3VXHQR7)."
|
|
8661
|
+
);
|
|
8679
8662
|
}
|
|
8680
|
-
const
|
|
8681
|
-
if (
|
|
8682
|
-
throw new Error(`Invalid bot
|
|
8663
|
+
const idError = validateBotId(botId);
|
|
8664
|
+
if (idError !== null) {
|
|
8665
|
+
throw new Error(`Invalid bot ID: ${idError}`);
|
|
8683
8666
|
}
|
|
8667
|
+
const apiUrl = process.env["DESKFREE_API_URL"] ?? deriveApiUrl();
|
|
8684
8668
|
const apiUrlError = validateApiUrl(apiUrl);
|
|
8685
8669
|
if (apiUrlError !== null) {
|
|
8686
8670
|
throw new Error(`Invalid API URL: ${apiUrlError}`);
|
|
@@ -8695,9 +8679,13 @@ function loadConfig() {
|
|
|
8695
8679
|
if (isNaN(healthPort) || healthPort < 1 || healthPort > 65535) {
|
|
8696
8680
|
throw new Error(`Invalid HEALTH_PORT: ${healthPortRaw}`);
|
|
8697
8681
|
}
|
|
8682
|
+
const stage = process.env["STAGE"] ?? "dev";
|
|
8683
|
+
const wsUrl = process.env["DESKFREE_WS_URL"] ?? deriveWsUrl();
|
|
8698
8684
|
return {
|
|
8699
|
-
|
|
8685
|
+
botId,
|
|
8700
8686
|
apiUrl,
|
|
8687
|
+
stage,
|
|
8688
|
+
wsUrl,
|
|
8701
8689
|
stateDir: process.env["DESKFREE_STATE_DIR"] ?? DEFAULTS.stateDir,
|
|
8702
8690
|
toolsDir: process.env["DESKFREE_TOOLS_DIR"] ?? DEFAULTS.toolsDir,
|
|
8703
8691
|
logLevel,
|
|
@@ -8720,7 +8708,7 @@ function mergeWithRemoteConfig(local, remote) {
|
|
|
8720
8708
|
return {
|
|
8721
8709
|
...local,
|
|
8722
8710
|
claudeCodePath,
|
|
8723
|
-
wsUrl: process.env["DESKFREE_WS_URL"] ?? remote.wsUrl,
|
|
8711
|
+
wsUrl: process.env["DESKFREE_WS_URL"] ?? remote.wsUrl ?? local.wsUrl,
|
|
8724
8712
|
model: process.env["DESKFREE_MODEL"] ?? remote.model,
|
|
8725
8713
|
awsRegion: process.env["AWS_REGION"] ?? remote.awsRegion,
|
|
8726
8714
|
heartbeatIntervalMs: process.env["DESKFREE_HEARTBEAT_INTERVAL_MS"] ? parseInt(process.env["DESKFREE_HEARTBEAT_INTERVAL_MS"], 10) : remote.heartbeatIntervalMs,
|
|
@@ -12573,17 +12561,43 @@ var require_websocket_server2 = __commonJS({
|
|
|
12573
12561
|
});
|
|
12574
12562
|
|
|
12575
12563
|
// ../../node_modules/ws/wrapper.mjs
|
|
12576
|
-
var
|
|
12564
|
+
var wrapper_exports = {};
|
|
12565
|
+
__export(wrapper_exports, {
|
|
12566
|
+
Receiver: () => import_receiver2.default,
|
|
12567
|
+
Sender: () => import_sender2.default,
|
|
12568
|
+
WebSocket: () => import_websocket2.default,
|
|
12569
|
+
WebSocketServer: () => import_websocket_server2.default,
|
|
12570
|
+
createWebSocketStream: () => import_stream2.default,
|
|
12571
|
+
default: () => wrapper_default2
|
|
12572
|
+
});
|
|
12573
|
+
var import_stream2, import_receiver2, import_sender2, import_websocket2, import_websocket_server2, wrapper_default2;
|
|
12577
12574
|
var init_wrapper = __esm({
|
|
12578
12575
|
"../../node_modules/ws/wrapper.mjs"() {
|
|
12579
|
-
__toESM(require_stream2());
|
|
12580
|
-
__toESM(require_receiver2());
|
|
12581
|
-
__toESM(require_sender2());
|
|
12576
|
+
import_stream2 = __toESM(require_stream2());
|
|
12577
|
+
import_receiver2 = __toESM(require_receiver2());
|
|
12578
|
+
import_sender2 = __toESM(require_sender2());
|
|
12582
12579
|
import_websocket2 = __toESM(require_websocket2());
|
|
12583
|
-
__toESM(require_websocket_server2());
|
|
12580
|
+
import_websocket_server2 = __toESM(require_websocket_server2());
|
|
12584
12581
|
wrapper_default2 = import_websocket2.default;
|
|
12585
12582
|
}
|
|
12586
12583
|
});
|
|
12584
|
+
|
|
12585
|
+
// src/gateway/ws-gateway.ts
|
|
12586
|
+
var ws_gateway_exports = {};
|
|
12587
|
+
__export(ws_gateway_exports, {
|
|
12588
|
+
getRotationToken: () => getRotationToken,
|
|
12589
|
+
setInitialRotationToken: () => setInitialRotationToken,
|
|
12590
|
+
startGateway: () => startGateway
|
|
12591
|
+
});
|
|
12592
|
+
function getRotationToken() {
|
|
12593
|
+
if (!currentRotationToken) {
|
|
12594
|
+
throw new Error("No rotation token available \u2014 WS init not yet completed");
|
|
12595
|
+
}
|
|
12596
|
+
return currentRotationToken;
|
|
12597
|
+
}
|
|
12598
|
+
function setInitialRotationToken(token) {
|
|
12599
|
+
currentRotationToken = token;
|
|
12600
|
+
}
|
|
12587
12601
|
function nextBackoff(state2) {
|
|
12588
12602
|
const delay = Math.min(
|
|
12589
12603
|
BACKOFF_INITIAL_MS * Math.pow(BACKOFF_FACTOR, state2.attempt),
|
|
@@ -12609,6 +12623,89 @@ function sleepWithAbort(ms, signal) {
|
|
|
12609
12623
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
12610
12624
|
});
|
|
12611
12625
|
}
|
|
12626
|
+
async function wsReconnect(config, rotationToken) {
|
|
12627
|
+
const { botId, stage, wsUrl, log, abortSignal } = config;
|
|
12628
|
+
const params = new URLSearchParams({ id: botId, stage });
|
|
12629
|
+
if (rotationToken) {
|
|
12630
|
+
params.set("token", rotationToken);
|
|
12631
|
+
}
|
|
12632
|
+
const fullUrl = `${wsUrl}?${params.toString()}`;
|
|
12633
|
+
return new Promise(
|
|
12634
|
+
(resolve, reject) => {
|
|
12635
|
+
const ws = new wrapper_default2(fullUrl);
|
|
12636
|
+
let settled = false;
|
|
12637
|
+
let timeoutTimer;
|
|
12638
|
+
const cleanup = () => {
|
|
12639
|
+
if (timeoutTimer !== void 0) {
|
|
12640
|
+
clearTimeout(timeoutTimer);
|
|
12641
|
+
timeoutTimer = void 0;
|
|
12642
|
+
}
|
|
12643
|
+
};
|
|
12644
|
+
const fail = (err) => {
|
|
12645
|
+
if (settled) return;
|
|
12646
|
+
settled = true;
|
|
12647
|
+
cleanup();
|
|
12648
|
+
try {
|
|
12649
|
+
ws.close();
|
|
12650
|
+
} catch {
|
|
12651
|
+
}
|
|
12652
|
+
reject(err);
|
|
12653
|
+
};
|
|
12654
|
+
timeoutTimer = setTimeout(() => {
|
|
12655
|
+
fail(
|
|
12656
|
+
new Error(
|
|
12657
|
+
`WS reconnect timeout after ${WS_RECONNECT_INIT_TIMEOUT_MS}ms`
|
|
12658
|
+
)
|
|
12659
|
+
);
|
|
12660
|
+
}, WS_RECONNECT_INIT_TIMEOUT_MS);
|
|
12661
|
+
ws.on("open", () => {
|
|
12662
|
+
ws.send(JSON.stringify({ action: "init" }));
|
|
12663
|
+
});
|
|
12664
|
+
ws.on("message", (data) => {
|
|
12665
|
+
try {
|
|
12666
|
+
const msg = JSON.parse(data.toString());
|
|
12667
|
+
if (msg.action === "go" && msg.rotationToken) {
|
|
12668
|
+
if (settled) return;
|
|
12669
|
+
settled = true;
|
|
12670
|
+
cleanup();
|
|
12671
|
+
resolve({ ws, rotationToken: msg.rotationToken });
|
|
12672
|
+
} else if (msg.action === "rejected") {
|
|
12673
|
+
fail(
|
|
12674
|
+
new Error(
|
|
12675
|
+
`Reconnect rejected: ${msg.reason ?? "unknown reason"}`
|
|
12676
|
+
)
|
|
12677
|
+
);
|
|
12678
|
+
} else if (msg.action === "lobby") {
|
|
12679
|
+
log.info("Reconnect landed in lobby \u2014 awaiting approval...");
|
|
12680
|
+
cleanup();
|
|
12681
|
+
}
|
|
12682
|
+
} catch (err) {
|
|
12683
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12684
|
+
log.warn(`Error parsing reconnect response: ${errMsg}`);
|
|
12685
|
+
}
|
|
12686
|
+
});
|
|
12687
|
+
ws.on("error", (err) => {
|
|
12688
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
12689
|
+
});
|
|
12690
|
+
ws.on("close", (code, reason) => {
|
|
12691
|
+
if (!settled) {
|
|
12692
|
+
fail(
|
|
12693
|
+
new Error(
|
|
12694
|
+
`WebSocket closed during reconnect: ${code} ${reason.toString()}`
|
|
12695
|
+
)
|
|
12696
|
+
);
|
|
12697
|
+
}
|
|
12698
|
+
});
|
|
12699
|
+
abortSignal.addEventListener(
|
|
12700
|
+
"abort",
|
|
12701
|
+
() => {
|
|
12702
|
+
fail(new Error("Aborted during WS reconnect"));
|
|
12703
|
+
},
|
|
12704
|
+
{ once: true }
|
|
12705
|
+
);
|
|
12706
|
+
}
|
|
12707
|
+
);
|
|
12708
|
+
}
|
|
12612
12709
|
async function startGateway(config) {
|
|
12613
12710
|
const { client, accountId, stateDir, log, abortSignal } = config;
|
|
12614
12711
|
const ctx = { accountId };
|
|
@@ -12625,20 +12722,24 @@ async function startGateway(config) {
|
|
|
12625
12722
|
});
|
|
12626
12723
|
while (!abortSignal.aborted) {
|
|
12627
12724
|
try {
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12725
|
+
let ws;
|
|
12726
|
+
if (totalReconnects === 0) {
|
|
12727
|
+
ws = config.initialWs;
|
|
12728
|
+
currentRotationToken = config.initialRotationToken;
|
|
12729
|
+
log.info("Using initial WS connection from handshake.");
|
|
12730
|
+
} else {
|
|
12631
12731
|
log.info(
|
|
12632
|
-
`
|
|
12732
|
+
`Reconnecting to ${config.wsUrl}... (reconnect #${totalReconnects})`
|
|
12633
12733
|
);
|
|
12734
|
+
const result = await wsReconnect(config, currentRotationToken ?? "");
|
|
12735
|
+
ws = result.ws;
|
|
12736
|
+
currentRotationToken = result.rotationToken;
|
|
12634
12737
|
recordReconnect(accountId);
|
|
12635
|
-
} else {
|
|
12636
|
-
log.info(`Got WS ticket, connecting to ${wsUrl}...`);
|
|
12637
12738
|
}
|
|
12739
|
+
resetBackoff(backoff);
|
|
12638
12740
|
updateHealthMode(accountId, "websocket");
|
|
12639
12741
|
cursor = await runWebSocketConnection({
|
|
12640
|
-
|
|
12641
|
-
wsUrl,
|
|
12742
|
+
ws,
|
|
12642
12743
|
client,
|
|
12643
12744
|
accountId,
|
|
12644
12745
|
stateDir,
|
|
@@ -12653,26 +12754,16 @@ async function startGateway(config) {
|
|
|
12653
12754
|
} catch (err) {
|
|
12654
12755
|
totalReconnects++;
|
|
12655
12756
|
const message = err instanceof Error ? err.message : String(err);
|
|
12656
|
-
|
|
12757
|
+
const isConnectFailure = message.includes("WS-first connect") || message.includes("WS reconnect") || message.includes("Connection rejected") || message.includes("Reconnect rejected");
|
|
12758
|
+
if (isConnectFailure) {
|
|
12657
12759
|
log.warn(
|
|
12658
|
-
`
|
|
12760
|
+
`Connection setup failed (attempt #${totalReconnects}): ${message}. Will retry after backoff.`
|
|
12659
12761
|
);
|
|
12660
|
-
reportError("error", `
|
|
12762
|
+
reportError("error", `Connection setup failed: ${message}`, {
|
|
12661
12763
|
component: "gateway",
|
|
12662
|
-
event: "
|
|
12764
|
+
event: "ws_connect_failed",
|
|
12663
12765
|
attempt: totalReconnects
|
|
12664
12766
|
});
|
|
12665
|
-
recordReconnect(accountId);
|
|
12666
|
-
updateHealthMode(accountId, "polling");
|
|
12667
|
-
cursor = await runPollingFallback({
|
|
12668
|
-
client,
|
|
12669
|
-
accountId,
|
|
12670
|
-
stateDir,
|
|
12671
|
-
cursor,
|
|
12672
|
-
log,
|
|
12673
|
-
abortSignal,
|
|
12674
|
-
onMessage: config.onMessage
|
|
12675
|
-
});
|
|
12676
12767
|
} else {
|
|
12677
12768
|
log.warn(`Connection error (attempt #${totalReconnects}): ${message}`);
|
|
12678
12769
|
reportError("warn", `WS connection error: ${message}`, {
|
|
@@ -12680,8 +12771,8 @@ async function startGateway(config) {
|
|
|
12680
12771
|
event: "connection_error",
|
|
12681
12772
|
attempt: totalReconnects
|
|
12682
12773
|
});
|
|
12683
|
-
recordReconnect(accountId);
|
|
12684
12774
|
}
|
|
12775
|
+
recordReconnect(accountId);
|
|
12685
12776
|
if (abortSignal.aborted) break;
|
|
12686
12777
|
const delay = nextBackoff(backoff);
|
|
12687
12778
|
log.info(
|
|
@@ -12693,25 +12784,20 @@ async function startGateway(config) {
|
|
|
12693
12784
|
log.info(`Gateway loop exited after ${totalReconnects} reconnect(s).`);
|
|
12694
12785
|
}
|
|
12695
12786
|
async function runWebSocketConnection(opts) {
|
|
12696
|
-
const {
|
|
12787
|
+
const { client, accountId, stateDir, log, abortSignal } = opts;
|
|
12788
|
+
const ws = opts.ws;
|
|
12697
12789
|
const ctx = { accountId };
|
|
12698
12790
|
let cursor = opts.cursor;
|
|
12699
12791
|
return new Promise((resolve, reject) => {
|
|
12700
|
-
const ws = new wrapper_default2(`${wsUrl}?ticket=${ticket}`);
|
|
12701
12792
|
let pingInterval;
|
|
12702
|
-
let connectionTimer;
|
|
12703
12793
|
let pongTimer;
|
|
12704
12794
|
let notifyDebounceTimer;
|
|
12705
|
-
let
|
|
12795
|
+
let proactiveReconnectTimer;
|
|
12706
12796
|
const cleanup = () => {
|
|
12707
12797
|
if (pingInterval !== void 0) {
|
|
12708
12798
|
clearInterval(pingInterval);
|
|
12709
12799
|
pingInterval = void 0;
|
|
12710
12800
|
}
|
|
12711
|
-
if (connectionTimer !== void 0) {
|
|
12712
|
-
clearTimeout(connectionTimer);
|
|
12713
|
-
connectionTimer = void 0;
|
|
12714
|
-
}
|
|
12715
12801
|
if (pongTimer !== void 0) {
|
|
12716
12802
|
clearTimeout(pongTimer);
|
|
12717
12803
|
pongTimer = void 0;
|
|
@@ -12720,78 +12806,72 @@ async function runWebSocketConnection(opts) {
|
|
|
12720
12806
|
clearTimeout(notifyDebounceTimer);
|
|
12721
12807
|
notifyDebounceTimer = void 0;
|
|
12722
12808
|
}
|
|
12809
|
+
if (proactiveReconnectTimer !== void 0) {
|
|
12810
|
+
clearTimeout(proactiveReconnectTimer);
|
|
12811
|
+
proactiveReconnectTimer = void 0;
|
|
12812
|
+
}
|
|
12723
12813
|
};
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12814
|
+
setActiveWs(ws);
|
|
12815
|
+
log.info("WebSocket session started.");
|
|
12816
|
+
setWsConnected(true);
|
|
12817
|
+
setHealthMode("websocket");
|
|
12818
|
+
pingInterval = setInterval(() => {
|
|
12819
|
+
if (ws.readyState === wrapper_default2.OPEN) {
|
|
12727
12820
|
try {
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
reject(
|
|
12732
|
-
new Error(
|
|
12733
|
-
`WebSocket connection timeout after ${WS_CONNECTION_TIMEOUT_MS}ms`
|
|
12734
|
-
)
|
|
12735
|
-
);
|
|
12736
|
-
}
|
|
12737
|
-
}, WS_CONNECTION_TIMEOUT_MS);
|
|
12738
|
-
ws.on("open", () => {
|
|
12739
|
-
isConnected = true;
|
|
12740
|
-
setActiveWs(ws);
|
|
12741
|
-
if (connectionTimer !== void 0) {
|
|
12742
|
-
clearTimeout(connectionTimer);
|
|
12743
|
-
connectionTimer = void 0;
|
|
12744
|
-
}
|
|
12745
|
-
log.info("WebSocket connected.");
|
|
12746
|
-
setWsConnected(true);
|
|
12747
|
-
setHealthMode("websocket");
|
|
12748
|
-
pingInterval = setInterval(() => {
|
|
12749
|
-
if (ws.readyState === wrapper_default2.OPEN) {
|
|
12750
|
-
try {
|
|
12751
|
-
ws.send(JSON.stringify({ action: "ping" }));
|
|
12752
|
-
pongTimer = setTimeout(() => {
|
|
12753
|
-
log.warn("Pong timeout \u2014 closing WebSocket");
|
|
12754
|
-
try {
|
|
12755
|
-
ws.close(1002, "pong timeout");
|
|
12756
|
-
} catch {
|
|
12757
|
-
}
|
|
12758
|
-
}, WS_PONG_TIMEOUT_MS);
|
|
12759
|
-
} catch (err) {
|
|
12760
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12761
|
-
log.warn(`Failed to send ping: ${msg}`);
|
|
12821
|
+
const pingPayload = { action: "ping" };
|
|
12822
|
+
if (currentRotationToken) {
|
|
12823
|
+
pingPayload.token = currentRotationToken;
|
|
12762
12824
|
}
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
...status2
|
|
12772
|
-
})
|
|
12773
|
-
);
|
|
12825
|
+
ws.send(JSON.stringify(pingPayload));
|
|
12826
|
+
pongTimer = setTimeout(() => {
|
|
12827
|
+
log.warn("Pong timeout \u2014 closing WebSocket");
|
|
12828
|
+
try {
|
|
12829
|
+
ws.close(1002, "pong timeout");
|
|
12830
|
+
} catch {
|
|
12831
|
+
}
|
|
12832
|
+
}, WS_PONG_TIMEOUT_MS);
|
|
12774
12833
|
} catch (err) {
|
|
12775
|
-
const
|
|
12776
|
-
log.warn(`Failed to send
|
|
12834
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12835
|
+
log.warn(`Failed to send ping: ${msg}`);
|
|
12777
12836
|
}
|
|
12778
12837
|
}
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12838
|
+
}, PING_INTERVAL_MS);
|
|
12839
|
+
proactiveReconnectTimer = setTimeout(() => {
|
|
12840
|
+
log.info("Proactive reconnect at 1h50m \u2014 closing for reconnect");
|
|
12841
|
+
try {
|
|
12842
|
+
ws.close(1e3, "proactive_reconnect");
|
|
12843
|
+
} catch {
|
|
12844
|
+
}
|
|
12845
|
+
}, PROACTIVE_RECONNECT_MS);
|
|
12846
|
+
if (opts.getWorkerStatus) {
|
|
12847
|
+
try {
|
|
12848
|
+
const status2 = opts.getWorkerStatus();
|
|
12849
|
+
ws.send(
|
|
12850
|
+
JSON.stringify({
|
|
12851
|
+
action: "heartbeatResponse",
|
|
12852
|
+
...status2
|
|
12853
|
+
})
|
|
12854
|
+
);
|
|
12855
|
+
} catch (err) {
|
|
12856
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12857
|
+
log.warn(`Failed to send initial heartbeat: ${errMsg}`);
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12860
|
+
void pollAndDeliver(
|
|
12861
|
+
client,
|
|
12862
|
+
accountId,
|
|
12863
|
+
stateDir,
|
|
12864
|
+
cursor,
|
|
12865
|
+
opts.onMessage,
|
|
12866
|
+
log
|
|
12867
|
+
).then((result) => {
|
|
12868
|
+
if (result.cursor) {
|
|
12869
|
+
cursor = result.cursor;
|
|
12870
|
+
saveCursor(ctx, cursor, stateDir, log);
|
|
12871
|
+
}
|
|
12872
|
+
}).catch((err) => {
|
|
12873
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12874
|
+
log.warn(`Initial poll failed: ${msg}`);
|
|
12795
12875
|
});
|
|
12796
12876
|
ws.on("message", (data) => {
|
|
12797
12877
|
try {
|
|
@@ -12834,7 +12914,19 @@ async function runWebSocketConnection(opts) {
|
|
|
12834
12914
|
clearTimeout(pongTimer);
|
|
12835
12915
|
pongTimer = void 0;
|
|
12836
12916
|
}
|
|
12837
|
-
|
|
12917
|
+
const pongMsg = msg;
|
|
12918
|
+
if (pongMsg.rotationToken) {
|
|
12919
|
+
currentRotationToken = pongMsg.rotationToken;
|
|
12920
|
+
log.debug("Received pong \u2014 token rotated");
|
|
12921
|
+
} else if (pongMsg.error === "rotation_invalid") {
|
|
12922
|
+
log.warn("Rotation token invalid \u2014 closing for reconnect");
|
|
12923
|
+
try {
|
|
12924
|
+
ws.close(1e3, "rotation_invalid");
|
|
12925
|
+
} catch {
|
|
12926
|
+
}
|
|
12927
|
+
} else {
|
|
12928
|
+
log.debug("Received pong \u2014 connection healthy");
|
|
12929
|
+
}
|
|
12838
12930
|
} else if (msg.action === "heartbeatRequest") {
|
|
12839
12931
|
if (opts.getWorkerStatus && ws.readyState === wrapper_default2.OPEN) {
|
|
12840
12932
|
try {
|
|
@@ -12936,7 +13028,6 @@ async function runWebSocketConnection(opts) {
|
|
|
12936
13028
|
});
|
|
12937
13029
|
ws.on("close", (code, reason) => {
|
|
12938
13030
|
cleanup();
|
|
12939
|
-
isConnected = false;
|
|
12940
13031
|
setActiveWs(null);
|
|
12941
13032
|
setWsConnected(false);
|
|
12942
13033
|
if (code === 1e3 || code === 1001) {
|
|
@@ -12970,7 +13061,6 @@ async function runWebSocketConnection(opts) {
|
|
|
12970
13061
|
});
|
|
12971
13062
|
ws.on("error", (err) => {
|
|
12972
13063
|
cleanup();
|
|
12973
|
-
isConnected = false;
|
|
12974
13064
|
setActiveWs(null);
|
|
12975
13065
|
setWsConnected(false);
|
|
12976
13066
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -12999,50 +13089,7 @@ async function runWebSocketConnection(opts) {
|
|
|
12999
13089
|
);
|
|
13000
13090
|
});
|
|
13001
13091
|
}
|
|
13002
|
-
|
|
13003
|
-
const { client, accountId, stateDir, log, abortSignal } = opts;
|
|
13004
|
-
let cursor = opts.cursor;
|
|
13005
|
-
let iterations = 0;
|
|
13006
|
-
let consecutiveFailures = 0;
|
|
13007
|
-
log.info("Running in polling fallback mode.");
|
|
13008
|
-
setHealthMode("polling");
|
|
13009
|
-
while (!abortSignal.aborted && iterations < MAX_POLLING_ITERATIONS) {
|
|
13010
|
-
const result = await pollAndDeliver(
|
|
13011
|
-
client,
|
|
13012
|
-
accountId,
|
|
13013
|
-
stateDir,
|
|
13014
|
-
cursor,
|
|
13015
|
-
opts.onMessage,
|
|
13016
|
-
log
|
|
13017
|
-
);
|
|
13018
|
-
if (result.ok) {
|
|
13019
|
-
if (result.cursor) cursor = result.cursor;
|
|
13020
|
-
consecutiveFailures = 0;
|
|
13021
|
-
} else {
|
|
13022
|
-
consecutiveFailures++;
|
|
13023
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
13024
|
-
log.warn(
|
|
13025
|
-
`${consecutiveFailures} consecutive poll failures, breaking to retry WebSocket`
|
|
13026
|
-
);
|
|
13027
|
-
reportError(
|
|
13028
|
-
"error",
|
|
13029
|
-
`${consecutiveFailures} consecutive poll failures, switching to WS retry`,
|
|
13030
|
-
{
|
|
13031
|
-
component: "gateway",
|
|
13032
|
-
event: "poll_max_failures",
|
|
13033
|
-
consecutiveFailures
|
|
13034
|
-
}
|
|
13035
|
-
);
|
|
13036
|
-
break;
|
|
13037
|
-
}
|
|
13038
|
-
}
|
|
13039
|
-
iterations++;
|
|
13040
|
-
const jitter = Math.random() * POLL_FALLBACK_INTERVAL_MS * 0.2;
|
|
13041
|
-
await sleepWithAbort(POLL_FALLBACK_INTERVAL_MS + jitter, abortSignal);
|
|
13042
|
-
}
|
|
13043
|
-
return cursor;
|
|
13044
|
-
}
|
|
13045
|
-
var PING_INTERVAL_MS, POLL_FALLBACK_INTERVAL_MS, WS_CONNECTION_TIMEOUT_MS, WS_PONG_TIMEOUT_MS, NOTIFY_DEBOUNCE_MS, HEALTH_LOG_INTERVAL_MS, MAX_POLLING_ITERATIONS, MAX_CONSECUTIVE_POLL_FAILURES, BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_FACTOR;
|
|
13092
|
+
var PING_INTERVAL_MS, WS_PONG_TIMEOUT_MS, NOTIFY_DEBOUNCE_MS, HEALTH_LOG_INTERVAL_MS, PROACTIVE_RECONNECT_MS, WS_RECONNECT_INIT_TIMEOUT_MS, BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_FACTOR, currentRotationToken;
|
|
13046
13093
|
var init_ws_gateway = __esm({
|
|
13047
13094
|
"src/gateway/ws-gateway.ts"() {
|
|
13048
13095
|
init_health_state();
|
|
@@ -13051,16 +13098,15 @@ var init_ws_gateway = __esm({
|
|
|
13051
13098
|
init_dist();
|
|
13052
13099
|
init_wrapper();
|
|
13053
13100
|
PING_INTERVAL_MS = 5 * 60 * 1e3;
|
|
13054
|
-
POLL_FALLBACK_INTERVAL_MS = 3e4;
|
|
13055
|
-
WS_CONNECTION_TIMEOUT_MS = 3e4;
|
|
13056
13101
|
WS_PONG_TIMEOUT_MS = 1e4;
|
|
13057
13102
|
NOTIFY_DEBOUNCE_MS = 200;
|
|
13058
13103
|
HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
|
|
13059
|
-
|
|
13060
|
-
|
|
13104
|
+
PROACTIVE_RECONNECT_MS = 110 * 60 * 1e3;
|
|
13105
|
+
WS_RECONNECT_INIT_TIMEOUT_MS = 3e4;
|
|
13061
13106
|
BACKOFF_INITIAL_MS = 2e3;
|
|
13062
13107
|
BACKOFF_MAX_MS = 3e4;
|
|
13063
13108
|
BACKOFF_FACTOR = 1.8;
|
|
13109
|
+
currentRotationToken = null;
|
|
13064
13110
|
}
|
|
13065
13111
|
});
|
|
13066
13112
|
function jsonSchemaPropertyToZod(prop) {
|
|
@@ -13274,13 +13320,13 @@ function validateDownloadUrl(url) {
|
|
|
13274
13320
|
if (parsed.protocol !== "https:") {
|
|
13275
13321
|
throw new Error("Only HTTPS URLs are allowed");
|
|
13276
13322
|
}
|
|
13277
|
-
const
|
|
13278
|
-
const bareHostname =
|
|
13323
|
+
const hostname2 = parsed.hostname.toLowerCase();
|
|
13324
|
+
const bareHostname = hostname2.replace(/^\[|\]$/g, "");
|
|
13279
13325
|
if (bareHostname === "localhost" || bareHostname === "127.0.0.1" || bareHostname === "::1") {
|
|
13280
13326
|
throw new Error("Local URLs are not allowed");
|
|
13281
13327
|
}
|
|
13282
13328
|
for (const pattern of PRIVATE_IPV4_PATTERNS) {
|
|
13283
|
-
if (pattern.test(
|
|
13329
|
+
if (pattern.test(hostname2)) {
|
|
13284
13330
|
throw new Error("Private IP addresses are not allowed");
|
|
13285
13331
|
}
|
|
13286
13332
|
}
|
|
@@ -13850,8 +13896,9 @@ async function routeMessage(message, client, deps, sessionStore, config) {
|
|
|
13850
13896
|
`Auto-reopening task ${message.taskId} (attachment on ${task.status} task)`
|
|
13851
13897
|
);
|
|
13852
13898
|
try {
|
|
13853
|
-
await client.
|
|
13899
|
+
await client.updateTaskStatus({
|
|
13854
13900
|
taskId: message.taskId,
|
|
13901
|
+
status: "open",
|
|
13855
13902
|
reason: "Human sent an attachment \u2014 reopening for further work"
|
|
13856
13903
|
});
|
|
13857
13904
|
routingTarget = "orchestrator";
|
|
@@ -14603,6 +14650,17 @@ ${userMessage}
|
|
|
14603
14650
|
} catch {
|
|
14604
14651
|
}
|
|
14605
14652
|
} finally {
|
|
14653
|
+
try {
|
|
14654
|
+
const task = await client.getTask({ taskId });
|
|
14655
|
+
if (task.status === "open" && task.awaiting === "bot") {
|
|
14656
|
+
log.info(
|
|
14657
|
+
`Worker finished but task ${taskId} still awaiting bot \u2014 auto-flipping to human`
|
|
14658
|
+
);
|
|
14659
|
+
await client.updateTaskStatus({ taskId, awaiting: "human" });
|
|
14660
|
+
}
|
|
14661
|
+
} catch {
|
|
14662
|
+
log.warn(`Failed to check/flip awaiting state for task ${taskId}`);
|
|
14663
|
+
}
|
|
14606
14664
|
const handle = this.workers.get(taskId);
|
|
14607
14665
|
if (handle) {
|
|
14608
14666
|
clearTimeout(handle.idleTimer);
|
|
@@ -14659,6 +14717,59 @@ ${userMessage}
|
|
|
14659
14717
|
}
|
|
14660
14718
|
});
|
|
14661
14719
|
|
|
14720
|
+
// src/auth/fingerprint.ts
|
|
14721
|
+
var fingerprint_exports = {};
|
|
14722
|
+
__export(fingerprint_exports, {
|
|
14723
|
+
collectFingerprint: () => collectFingerprint
|
|
14724
|
+
});
|
|
14725
|
+
function collectFingerprint(stateDir, runtimeVersion) {
|
|
14726
|
+
return {
|
|
14727
|
+
hostId: getOrCreateHostId(stateDir),
|
|
14728
|
+
machineId: getMachineId(),
|
|
14729
|
+
platform: platform(),
|
|
14730
|
+
arch: arch(),
|
|
14731
|
+
hostname: hostname(),
|
|
14732
|
+
cpuModel: cpus()[0]?.model ?? "unknown",
|
|
14733
|
+
totalMemory: totalmem(),
|
|
14734
|
+
nodeVersion: process.version,
|
|
14735
|
+
runtimeVersion
|
|
14736
|
+
};
|
|
14737
|
+
}
|
|
14738
|
+
function getOrCreateHostId(stateDir) {
|
|
14739
|
+
const hostIdPath = join(stateDir, "host-id");
|
|
14740
|
+
if (existsSync(hostIdPath)) {
|
|
14741
|
+
const existing = readFileSync(hostIdPath, "utf8").trim();
|
|
14742
|
+
if (existing) return existing;
|
|
14743
|
+
}
|
|
14744
|
+
const hostId = randomUUID();
|
|
14745
|
+
mkdirSync(dirname(hostIdPath), { recursive: true });
|
|
14746
|
+
writeFileSync(hostIdPath, hostId, "utf8");
|
|
14747
|
+
return hostId;
|
|
14748
|
+
}
|
|
14749
|
+
function getMachineId() {
|
|
14750
|
+
try {
|
|
14751
|
+
if (platform() === "linux") {
|
|
14752
|
+
if (existsSync("/etc/machine-id")) {
|
|
14753
|
+
return readFileSync("/etc/machine-id", "utf8").trim();
|
|
14754
|
+
}
|
|
14755
|
+
}
|
|
14756
|
+
if (platform() === "darwin") {
|
|
14757
|
+
const output = execSync(
|
|
14758
|
+
"ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
|
|
14759
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
14760
|
+
);
|
|
14761
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
14762
|
+
if (match?.[1]) return match[1];
|
|
14763
|
+
}
|
|
14764
|
+
} catch {
|
|
14765
|
+
}
|
|
14766
|
+
return "unknown";
|
|
14767
|
+
}
|
|
14768
|
+
var init_fingerprint = __esm({
|
|
14769
|
+
"src/auth/fingerprint.ts"() {
|
|
14770
|
+
}
|
|
14771
|
+
});
|
|
14772
|
+
|
|
14662
14773
|
// src/service/entrypoint.ts
|
|
14663
14774
|
var entrypoint_exports = {};
|
|
14664
14775
|
__export(entrypoint_exports, {
|
|
@@ -14733,12 +14844,111 @@ function scheduleHeartbeat(createOrchServer, model, intervalMs, signal, log, cla
|
|
|
14733
14844
|
setTimeout(() => void tick(), intervalMs);
|
|
14734
14845
|
log.info(`Heartbeat scheduled every ${Math.round(intervalMs / 1e3)}s`);
|
|
14735
14846
|
}
|
|
14847
|
+
async function initialConnect(opts) {
|
|
14848
|
+
const { botId, stage, wsUrl, fingerprint, log, abortSignal } = opts;
|
|
14849
|
+
const WebSocket2 = (await Promise.resolve().then(() => (init_wrapper(), wrapper_exports))).default;
|
|
14850
|
+
const params = new URLSearchParams({ id: botId, stage });
|
|
14851
|
+
const fullUrl = `${wsUrl}?${params.toString()}`;
|
|
14852
|
+
return new Promise((resolve, reject) => {
|
|
14853
|
+
const ws = new WebSocket2(fullUrl);
|
|
14854
|
+
let settled = false;
|
|
14855
|
+
let timeoutTimer;
|
|
14856
|
+
const cleanup = () => {
|
|
14857
|
+
if (timeoutTimer !== void 0) {
|
|
14858
|
+
clearTimeout(timeoutTimer);
|
|
14859
|
+
timeoutTimer = void 0;
|
|
14860
|
+
}
|
|
14861
|
+
};
|
|
14862
|
+
const fail = (err) => {
|
|
14863
|
+
if (settled) return;
|
|
14864
|
+
settled = true;
|
|
14865
|
+
cleanup();
|
|
14866
|
+
try {
|
|
14867
|
+
ws.close();
|
|
14868
|
+
} catch {
|
|
14869
|
+
}
|
|
14870
|
+
reject(err);
|
|
14871
|
+
};
|
|
14872
|
+
timeoutTimer = setTimeout(() => {
|
|
14873
|
+
fail(new Error(`WS-first connect timeout after ${WS_INIT_TIMEOUT_MS}ms`));
|
|
14874
|
+
}, WS_INIT_TIMEOUT_MS);
|
|
14875
|
+
ws.on("open", () => {
|
|
14876
|
+
const initMsg = { action: "init" };
|
|
14877
|
+
{
|
|
14878
|
+
initMsg.fingerprint = fingerprint;
|
|
14879
|
+
}
|
|
14880
|
+
ws.send(JSON.stringify(initMsg));
|
|
14881
|
+
});
|
|
14882
|
+
ws.on("message", (data) => {
|
|
14883
|
+
try {
|
|
14884
|
+
const msg = JSON.parse(data.toString());
|
|
14885
|
+
if (msg.action === "go" && msg.rotationToken) {
|
|
14886
|
+
if (settled) return;
|
|
14887
|
+
settled = true;
|
|
14888
|
+
cleanup();
|
|
14889
|
+
ws.removeAllListeners();
|
|
14890
|
+
resolve({ ws, rotationToken: msg.rotationToken });
|
|
14891
|
+
} else if (msg.action === "lobby") {
|
|
14892
|
+
log.info("Awaiting human approval...");
|
|
14893
|
+
cleanup();
|
|
14894
|
+
} else if (msg.action === "rejected") {
|
|
14895
|
+
fail(
|
|
14896
|
+
new Error(`Connection rejected: ${msg.reason ?? "unknown reason"}`)
|
|
14897
|
+
);
|
|
14898
|
+
}
|
|
14899
|
+
} catch (err) {
|
|
14900
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
14901
|
+
log.warn(`Error parsing init response: ${errMsg}`);
|
|
14902
|
+
}
|
|
14903
|
+
});
|
|
14904
|
+
ws.on("error", (err) => {
|
|
14905
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
14906
|
+
});
|
|
14907
|
+
ws.on("close", (code, reason) => {
|
|
14908
|
+
if (!settled) {
|
|
14909
|
+
fail(
|
|
14910
|
+
new Error(
|
|
14911
|
+
`WebSocket closed during init: ${code} ${reason.toString()}`
|
|
14912
|
+
)
|
|
14913
|
+
);
|
|
14914
|
+
}
|
|
14915
|
+
});
|
|
14916
|
+
abortSignal.addEventListener(
|
|
14917
|
+
"abort",
|
|
14918
|
+
() => {
|
|
14919
|
+
fail(new Error("Aborted during WS-first connect"));
|
|
14920
|
+
},
|
|
14921
|
+
{ once: true }
|
|
14922
|
+
);
|
|
14923
|
+
});
|
|
14924
|
+
}
|
|
14736
14925
|
async function startAgent(opts) {
|
|
14737
14926
|
const localConfig = loadConfig();
|
|
14738
14927
|
const log = opts?.log ?? createLogger("agent", localConfig.logLevel);
|
|
14739
14928
|
const abortController = new AbortController();
|
|
14740
14929
|
log.info("DeskFree Agent Runtime starting...");
|
|
14741
|
-
const
|
|
14930
|
+
const { getRotationToken: getRotationToken2, setInitialRotationToken: setInitialRotationToken2 } = await Promise.resolve().then(() => (init_ws_gateway(), ws_gateway_exports));
|
|
14931
|
+
const { collectFingerprint: collectFingerprint2 } = await Promise.resolve().then(() => (init_fingerprint(), fingerprint_exports));
|
|
14932
|
+
const { createRequire } = await import('module');
|
|
14933
|
+
const require3 = createRequire(import.meta.url);
|
|
14934
|
+
const runtimePkg = require3("../../package.json");
|
|
14935
|
+
const runtimeVersion = runtimePkg.version;
|
|
14936
|
+
const fingerprint = collectFingerprint2(localConfig.stateDir, runtimeVersion);
|
|
14937
|
+
log.info("Connecting to DeskFree...", { wsUrl: localConfig.wsUrl });
|
|
14938
|
+
const connectResult = await initialConnect({
|
|
14939
|
+
botId: localConfig.botId,
|
|
14940
|
+
stage: localConfig.stage,
|
|
14941
|
+
wsUrl: localConfig.wsUrl,
|
|
14942
|
+
fingerprint,
|
|
14943
|
+
log,
|
|
14944
|
+
abortSignal: abortController.signal
|
|
14945
|
+
});
|
|
14946
|
+
setInitialRotationToken2(connectResult.rotationToken);
|
|
14947
|
+
log.info("Connected \u2014 got rotation token via WS handshake.");
|
|
14948
|
+
const client = new DeskFreeClient({
|
|
14949
|
+
apiUrl: localConfig.apiUrl,
|
|
14950
|
+
getToken: () => getRotationToken2()
|
|
14951
|
+
});
|
|
14742
14952
|
const errorReporter = initErrorReporter(client, log);
|
|
14743
14953
|
initializeHealth("unknown");
|
|
14744
14954
|
log.info("Bootstrapping from API...", { apiUrl: localConfig.apiUrl });
|
|
@@ -14758,7 +14968,6 @@ async function startAgent(opts) {
|
|
|
14758
14968
|
throw new Error(`Failed to bootstrap config from API: ${msg}`);
|
|
14759
14969
|
}
|
|
14760
14970
|
const isDocker2 = process.env["DOCKER"] === "1" || (await import('fs')).existsSync("/.dockerenv");
|
|
14761
|
-
const runtimeVersion = process.env["npm_package_version"] ?? "unknown";
|
|
14762
14971
|
const agentContext = {
|
|
14763
14972
|
botName: config.botName,
|
|
14764
14973
|
deploymentType: config.deploymentType,
|
|
@@ -14830,7 +15039,7 @@ async function startAgent(opts) {
|
|
|
14830
15039
|
const createOrchServer = () => createOrchestratorMcpServer(client, customTools, workerManager);
|
|
14831
15040
|
const healthServer = startHealthServer(config.healthPort, log);
|
|
14832
15041
|
const sessionStore = new SessionStore();
|
|
14833
|
-
log.info("Starting gateway", { wsUrl: config.wsUrl
|
|
15042
|
+
log.info("Starting gateway", { wsUrl: config.wsUrl });
|
|
14834
15043
|
void startGateway({
|
|
14835
15044
|
client,
|
|
14836
15045
|
wsUrl: config.wsUrl,
|
|
@@ -14838,6 +15047,10 @@ async function startAgent(opts) {
|
|
|
14838
15047
|
stateDir: config.stateDir,
|
|
14839
15048
|
log,
|
|
14840
15049
|
abortSignal: abortController.signal,
|
|
15050
|
+
botId: localConfig.botId,
|
|
15051
|
+
stage: localConfig.stage,
|
|
15052
|
+
initialWs: connectResult.ws,
|
|
15053
|
+
initialRotationToken: connectResult.rotationToken,
|
|
14841
15054
|
getWorkerStatus: () => ({
|
|
14842
15055
|
activeWorkers: workerManager.activeCount,
|
|
14843
15056
|
queuedTasks: workerManager.queuedCount,
|
|
@@ -14926,13 +15139,49 @@ async function startAgent(opts) {
|
|
|
14926
15139
|
`Sleep cycle: invoking sleep agent (${orientResult.entries.length} unconsolidated entries)...`
|
|
14927
15140
|
);
|
|
14928
15141
|
const workerServer = createWorkServer();
|
|
14929
|
-
const
|
|
15142
|
+
const agentStream = runOneShotWorker({
|
|
14930
15143
|
prompt,
|
|
14931
15144
|
systemPrompt: buildSleepDirective(agentContext),
|
|
14932
15145
|
workerServer,
|
|
14933
15146
|
model: config.model
|
|
14934
15147
|
});
|
|
14935
|
-
|
|
15148
|
+
let fullText = "";
|
|
15149
|
+
for await (const msg of agentStream) {
|
|
15150
|
+
if (msg.type === "assistant" && Array.isArray(msg.message?.content)) {
|
|
15151
|
+
for (const block of msg.message.content) {
|
|
15152
|
+
if (block.type === "text") {
|
|
15153
|
+
fullText += block.text;
|
|
15154
|
+
}
|
|
15155
|
+
}
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
const tagMatch = fullText.match(
|
|
15159
|
+
/<consolidation_result>([\s\S]*?)<\/consolidation_result>/
|
|
15160
|
+
);
|
|
15161
|
+
if (tagMatch) {
|
|
15162
|
+
try {
|
|
15163
|
+
const parsed = JSON.parse(tagMatch[1]);
|
|
15164
|
+
const consolidateResult = await client.consolidateMemory({
|
|
15165
|
+
newEntries: parsed.newEntries ?? [],
|
|
15166
|
+
modifications: parsed.modifications ?? [],
|
|
15167
|
+
operatingMemory: parsed.operatingMemory ?? ""
|
|
15168
|
+
});
|
|
15169
|
+
log.info(
|
|
15170
|
+
`Sleep cycle: consolidation applied (${consolidateResult.opsApplied} ops, ${consolidateResult.entriesArchived} archived)`
|
|
15171
|
+
);
|
|
15172
|
+
if (consolidateResult.validationErrors?.length > 0) {
|
|
15173
|
+
log.warn(
|
|
15174
|
+
`Sleep cycle: validation errors: ${consolidateResult.validationErrors.join("; ")}`
|
|
15175
|
+
);
|
|
15176
|
+
}
|
|
15177
|
+
} catch (parseErr) {
|
|
15178
|
+
const parseMsg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
15179
|
+
log.warn(`Sleep cycle: failed to submit consolidation: ${parseMsg}`);
|
|
15180
|
+
}
|
|
15181
|
+
} else {
|
|
15182
|
+
log.warn(
|
|
15183
|
+
"Sleep cycle: agent did not produce <consolidation_result> tags"
|
|
15184
|
+
);
|
|
14936
15185
|
}
|
|
14937
15186
|
return "success";
|
|
14938
15187
|
} finally {
|
|
@@ -15007,6 +15256,7 @@ async function startAgent(opts) {
|
|
|
15007
15256
|
log.info("Shutdown complete.");
|
|
15008
15257
|
};
|
|
15009
15258
|
}
|
|
15259
|
+
var WS_INIT_TIMEOUT_MS;
|
|
15010
15260
|
var init_entrypoint = __esm({
|
|
15011
15261
|
"src/service/entrypoint.ts"() {
|
|
15012
15262
|
init_orchestrator();
|
|
@@ -15023,6 +15273,7 @@ var init_entrypoint = __esm({
|
|
|
15023
15273
|
init_sessions();
|
|
15024
15274
|
init_worker_manager();
|
|
15025
15275
|
init_dist();
|
|
15276
|
+
WS_INIT_TIMEOUT_MS = 3e4;
|
|
15026
15277
|
}
|
|
15027
15278
|
});
|
|
15028
15279
|
|
|
@@ -15031,29 +15282,42 @@ init_paths();
|
|
|
15031
15282
|
var [name, cleanArgs] = parseName(process.argv.slice(2));
|
|
15032
15283
|
var command = cleanArgs[0];
|
|
15033
15284
|
if (command === "install") {
|
|
15034
|
-
|
|
15035
|
-
|
|
15285
|
+
const stageIdx = cleanArgs.indexOf("--stage");
|
|
15286
|
+
let stage;
|
|
15287
|
+
let installArgs = cleanArgs.slice(1);
|
|
15288
|
+
if (stageIdx !== -1) {
|
|
15289
|
+
stage = cleanArgs[stageIdx + 1];
|
|
15290
|
+
if (!stage || stage.startsWith("-")) {
|
|
15291
|
+
console.error("Error: --stage requires a value (e.g. --stage charlie)");
|
|
15292
|
+
process.exit(1);
|
|
15293
|
+
}
|
|
15294
|
+
installArgs = installArgs.filter(
|
|
15295
|
+
(_, i) => i !== stageIdx - 1 && i !== stageIdx
|
|
15296
|
+
);
|
|
15297
|
+
}
|
|
15298
|
+
let botId = installArgs[0];
|
|
15299
|
+
if (!botId) {
|
|
15036
15300
|
const { createInterface } = await import('readline');
|
|
15037
15301
|
const rl = createInterface({
|
|
15038
15302
|
input: process.stdin,
|
|
15039
15303
|
output: process.stdout
|
|
15040
15304
|
});
|
|
15041
|
-
|
|
15305
|
+
botId = await new Promise((resolve) => {
|
|
15042
15306
|
rl.question(
|
|
15043
|
-
"Paste your bot
|
|
15307
|
+
"Paste your bot ID (from the DeskFree dashboard):\n> ",
|
|
15044
15308
|
(answer) => {
|
|
15045
15309
|
rl.close();
|
|
15046
15310
|
resolve(answer.trim());
|
|
15047
15311
|
}
|
|
15048
15312
|
);
|
|
15049
15313
|
});
|
|
15050
|
-
if (!
|
|
15051
|
-
console.error("No
|
|
15314
|
+
if (!botId) {
|
|
15315
|
+
console.error("No bot ID provided.");
|
|
15052
15316
|
process.exit(1);
|
|
15053
15317
|
}
|
|
15054
15318
|
}
|
|
15055
15319
|
const { install: install2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
15056
|
-
install2(
|
|
15320
|
+
install2(botId, name, stage);
|
|
15057
15321
|
} else if (command === "uninstall") {
|
|
15058
15322
|
const { uninstall: uninstall2 } = await Promise.resolve().then(() => (init_uninstall(), uninstall_exports));
|
|
15059
15323
|
uninstall2(name);
|
|
@@ -15079,14 +15343,12 @@ if (command === "install") {
|
|
|
15079
15343
|
process.exit(1);
|
|
15080
15344
|
}, 5e3).unref();
|
|
15081
15345
|
};
|
|
15082
|
-
|
|
15083
|
-
if (
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
if (token && !process.env["DESKFREE_LAUNCH"]) {
|
|
15089
|
-
process.env["DESKFREE_LAUNCH"] = token;
|
|
15346
|
+
const startArgs = command === "start" ? cleanArgs.slice(1) : cleanArgs;
|
|
15347
|
+
if (startArgs.length >= 2) {
|
|
15348
|
+
process.env["STAGE"] = startArgs[0];
|
|
15349
|
+
process.env["BOT"] = startArgs[1];
|
|
15350
|
+
} else if (startArgs.length === 1) {
|
|
15351
|
+
process.env["BOT"] = startArgs[0];
|
|
15090
15352
|
}
|
|
15091
15353
|
const { startAgent: startAgent2 } = await Promise.resolve().then(() => (init_entrypoint(), entrypoint_exports));
|
|
15092
15354
|
const { createLogger: createLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
|