@questionbase/deskfree 0.6.2 → 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 +459 -485
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +14 -25
- package/dist/index.js +458 -484
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -2274,34 +2274,33 @@ function createOrchestratorTools(client, _options) {
|
|
|
2274
2274
|
return errorResult(err);
|
|
2275
2275
|
}
|
|
2276
2276
|
}),
|
|
2277
|
-
createTool(ORCHESTRATOR_TOOLS.
|
|
2277
|
+
createTool(ORCHESTRATOR_TOOLS.CREATE_TASK, async (params) => {
|
|
2278
2278
|
try {
|
|
2279
|
-
const
|
|
2280
|
-
const
|
|
2281
|
-
const
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
taskId
|
|
2279
|
+
const title = validateStringParam(params, "title", true);
|
|
2280
|
+
const instructions = validateStringParam(params, "instructions", false);
|
|
2281
|
+
const suggestedByTaskId = validateStringParam(
|
|
2282
|
+
params,
|
|
2283
|
+
"suggestedByTaskId",
|
|
2284
|
+
false
|
|
2285
|
+
);
|
|
2286
|
+
const inputFileIds = Array.isArray(params.inputFileIds) ? params.inputFileIds : void 0;
|
|
2287
|
+
const outputFileIds = Array.isArray(params.outputFileIds) ? params.outputFileIds : void 0;
|
|
2288
|
+
const scheduledFor = validateStringParam(params, "scheduledFor", false);
|
|
2289
|
+
const estimatedTokens = typeof params.estimatedTokens === "number" ? params.estimatedTokens : void 0;
|
|
2290
|
+
const result = await client.createTask({
|
|
2291
|
+
title,
|
|
2292
|
+
instructions,
|
|
2293
|
+
estimatedTokens,
|
|
2294
|
+
scheduledFor,
|
|
2295
|
+
inputFileIds,
|
|
2296
|
+
outputFileIds,
|
|
2297
|
+
suggestedByTaskId
|
|
2299
2298
|
});
|
|
2300
2299
|
return {
|
|
2301
2300
|
content: [
|
|
2302
2301
|
{
|
|
2303
2302
|
type: "text",
|
|
2304
|
-
text: `
|
|
2303
|
+
text: `Task created: "${result.title}" (${result.taskId})`
|
|
2305
2304
|
}
|
|
2306
2305
|
]
|
|
2307
2306
|
};
|
|
@@ -2519,34 +2518,33 @@ function createWorkerTools(client, options) {
|
|
|
2519
2518
|
return errorResult(err);
|
|
2520
2519
|
}
|
|
2521
2520
|
}),
|
|
2522
|
-
createTool(WORKER_TOOLS.
|
|
2521
|
+
createTool(WORKER_TOOLS.CREATE_TASK, async (params) => {
|
|
2523
2522
|
try {
|
|
2524
|
-
const
|
|
2525
|
-
const
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
taskId
|
|
2523
|
+
const title = validateStringParam(params, "title", true);
|
|
2524
|
+
const instructions = validateStringParam(params, "instructions", false);
|
|
2525
|
+
const suggestedByTaskId = validateStringParam(
|
|
2526
|
+
params,
|
|
2527
|
+
"suggestedByTaskId",
|
|
2528
|
+
false
|
|
2529
|
+
);
|
|
2530
|
+
const inputFileIds = Array.isArray(params.inputFileIds) ? params.inputFileIds : void 0;
|
|
2531
|
+
const outputFileIds = Array.isArray(params.outputFileIds) ? params.outputFileIds : void 0;
|
|
2532
|
+
const scheduledFor = validateStringParam(params, "scheduledFor", false);
|
|
2533
|
+
const estimatedTokens = typeof params.estimatedTokens === "number" ? params.estimatedTokens : void 0;
|
|
2534
|
+
const result = await client.createTask({
|
|
2535
|
+
title,
|
|
2536
|
+
instructions,
|
|
2537
|
+
estimatedTokens,
|
|
2538
|
+
scheduledFor,
|
|
2539
|
+
inputFileIds,
|
|
2540
|
+
outputFileIds,
|
|
2541
|
+
suggestedByTaskId
|
|
2544
2542
|
});
|
|
2545
2543
|
return {
|
|
2546
2544
|
content: [
|
|
2547
2545
|
{
|
|
2548
2546
|
type: "text",
|
|
2549
|
-
text: `
|
|
2547
|
+
text: `Task created: "${result.title}" (${result.taskId})`
|
|
2550
2548
|
}
|
|
2551
2549
|
]
|
|
2552
2550
|
};
|
|
@@ -2671,26 +2669,26 @@ function buildAgentDirective(ctx) {
|
|
|
2671
2669
|
|
|
2672
2670
|
## How You Work
|
|
2673
2671
|
|
|
2674
|
-
**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
|
|
2672
|
+
**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.
|
|
2675
2673
|
|
|
2676
2674
|
**The core loop:**
|
|
2677
2675
|
|
|
2678
2676
|
1. **Check state** \u2014 use \`deskfree_state\` to see tasks and files. Use \`deskfree_orient\` to recall relevant memories.
|
|
2679
|
-
2. **
|
|
2680
|
-
3. **Start work** \u2014 use \`deskfree_dispatch_worker\` with the taskId
|
|
2677
|
+
2. **Create tasks** \u2014 use \`deskfree_create_task\` to turn requests into concrete tasks.
|
|
2678
|
+
3. **Start work** \u2014 use \`deskfree_dispatch_worker\` with the taskId to work on the task in a thread.
|
|
2681
2679
|
4. **Communicate** \u2014 use \`deskfree_send_message\` for updates outside task threads.
|
|
2682
2680
|
|
|
2683
|
-
**Before
|
|
2684
|
-
- **One-off task** ("proofread this") \u2014
|
|
2685
|
-
- **New aspiration** ("I want to start posting on LinkedIn") \u2014 don't rush to
|
|
2686
|
-
- Never call \`
|
|
2681
|
+
**Before creating a task, qualify the request.** Figure out what kind of thing this is:
|
|
2682
|
+
- **One-off task** ("proofread this") \u2014 create a task directly.
|
|
2683
|
+
- **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.
|
|
2684
|
+
- Never call \`deskfree_create_task\` as your very first action \u2014 qualify first, even if briefly.
|
|
2687
2685
|
|
|
2688
2686
|
**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.
|
|
2689
2687
|
|
|
2690
|
-
In the main thread you
|
|
2688
|
+
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.
|
|
2691
2689
|
- When a human writes in a task thread, decide:
|
|
2692
2690
|
- **Continuation of the same task?** \u2192 reopen and pick it back up.
|
|
2693
|
-
- **New/different work request?** \u2192
|
|
2691
|
+
- **New/different work request?** \u2192 create it as a new task (don't reopen the old one).
|
|
2694
2692
|
- **Just confirmation or deferred?** \u2192 leave it for now.
|
|
2695
2693
|
- Estimate token cost per task \u2014 consider files to read, reasoning, output.
|
|
2696
2694
|
|
|
@@ -2714,7 +2712,7 @@ function buildWorkerDirective(ctx) {
|
|
|
2714
2712
|
## You're In a Task Thread
|
|
2715
2713
|
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.
|
|
2716
2714
|
|
|
2717
|
-
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,
|
|
2715
|
+
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.
|
|
2718
2716
|
|
|
2719
2717
|
**Context loading:**
|
|
2720
2718
|
- If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
|
|
@@ -2725,24 +2723,30 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
|
|
|
2725
2723
|
- If no pre-loaded context (edge case/fallback), call \`deskfree_start_task\` with your taskId to load it.
|
|
2726
2724
|
- 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.
|
|
2727
2725
|
|
|
2726
|
+
**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).
|
|
2727
|
+
|
|
2728
2728
|
**Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
|
|
2729
2729
|
|
|
2730
2730
|
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.
|
|
2731
|
-
2. **Align** \u2014
|
|
2731
|
+
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\`.
|
|
2732
2732
|
- **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.
|
|
2733
2733
|
- **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
|
|
2734
2734
|
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.
|
|
2735
|
-
4. **Deliver** \u2014 When work is ready for review,
|
|
2735
|
+
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.
|
|
2736
2736
|
|
|
2737
2737
|
**Push back when warranted:**
|
|
2738
2738
|
- 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.
|
|
2739
2739
|
- 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.
|
|
2740
2740
|
- You're a teammate, not a task executor. Have an opinion when you have the context to form one.
|
|
2741
2741
|
|
|
2742
|
+
**New requests mid-task:**
|
|
2743
|
+
- 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.
|
|
2744
|
+
- 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.
|
|
2745
|
+
- If you notice follow-up work yourself while executing, create a task immediately \u2014 don't wait until completion.
|
|
2746
|
+
|
|
2742
2747
|
**File rules:**
|
|
2743
2748
|
- Create files when your task naturally produces them. Don't be afraid to create multiple files if the work calls for it.
|
|
2744
2749
|
- Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
|
|
2745
|
-
- 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.
|
|
2746
2750
|
|
|
2747
2751
|
**Learnings \u2014 record aggressively:**
|
|
2748
2752
|
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.
|
|
@@ -2780,7 +2784,7 @@ On each heartbeat, run through this checklist:
|
|
|
2780
2784
|
|
|
2781
2785
|
### 1. Work the queue
|
|
2782
2786
|
- Run \`deskfree_state\` to get the full workspace snapshot.
|
|
2783
|
-
- **Check board load.** If there are 3+ tasks awaiting human review or input, skip proactive
|
|
2787
|
+
- **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.
|
|
2784
2788
|
- Any open tasks with awaiting=bot? Use \`deskfree_dispatch_worker\` to start working on each one. Pass the taskId.
|
|
2785
2789
|
- Any open tasks that seem stalled (no recent activity)? Check on them.
|
|
2786
2790
|
|
|
@@ -2795,7 +2799,7 @@ After handling the queue, step back and think about the bigger picture. You have
|
|
|
2795
2799
|
|
|
2796
2800
|
**Then act \u2014 but only if you have something genuinely useful:**
|
|
2797
2801
|
|
|
2798
|
-
*Things you can do* \u2014 research, drafts, analysis, prep.
|
|
2802
|
+
*Things you can do* \u2014 research, drafts, analysis, prep. Create a task via \`deskfree_create_task\`. One focused task, not a batch.
|
|
2799
2803
|
|
|
2800
2804
|
*Things the human should do* \u2014 nudges, reminders, conversation starters. Send via \`deskfree_send_message\`. Keep it brief and genuinely helpful, not nagging.
|
|
2801
2805
|
|
|
@@ -2811,7 +2815,7 @@ function buildSleepDirective(ctx) {
|
|
|
2811
2815
|
## Nightly Sleep Cycle \u2014 Memory Consolidation
|
|
2812
2816
|
You're running your nightly cycle to consolidate observations into long-term memory.
|
|
2813
2817
|
|
|
2814
|
-
Tools available: deskfree_state,
|
|
2818
|
+
Tools available: deskfree_state, deskfree_create_task, deskfree_send_message, deskfree_learning.
|
|
2815
2819
|
|
|
2816
2820
|
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.
|
|
2817
2821
|
|
|
@@ -2898,16 +2902,16 @@ Write a ~1500 token markdown summary with these sections:
|
|
|
2898
2902
|
After outputting the consolidation result:
|
|
2899
2903
|
1. Call \`deskfree_state\` to see the board.
|
|
2900
2904
|
2. Send a brief main-thread message via \`deskfree_send_message\` summarizing what was consolidated (1-2 sentences).
|
|
2901
|
-
3. Check for recurring commitments in operating memory \u2014
|
|
2902
|
-
4. One proactive
|
|
2905
|
+
3. Check for recurring commitments in operating memory \u2014 create via \`deskfree_create_task\` if needed.
|
|
2906
|
+
4. One proactive task max. Skip if nothing merits it or board is busy (3+ items needing human attention).`;
|
|
2903
2907
|
}
|
|
2904
2908
|
function buildDuskDirective(ctx) {
|
|
2905
2909
|
return `${identityBlock(ctx)}
|
|
2906
2910
|
|
|
2907
2911
|
## Evening Dusk Cycle
|
|
2908
|
-
You're running your evening cycle to review the day,
|
|
2912
|
+
You're running your evening cycle to review the day, create overnight tasks, and brief the human.
|
|
2909
2913
|
|
|
2910
|
-
Tools available: deskfree_state,
|
|
2914
|
+
Tools available: deskfree_state, deskfree_create_task, deskfree_send_message, deskfree_read_file, deskfree_orient.
|
|
2911
2915
|
|
|
2912
2916
|
---
|
|
2913
2917
|
|
|
@@ -2938,28 +2942,28 @@ Think about work that can be done autonomously overnight \u2014 WITHOUT human ju
|
|
|
2938
2942
|
- Creative work where the human has strong opinions on direction
|
|
2939
2943
|
- Anything the human explicitly said to wait on
|
|
2940
2944
|
|
|
2941
|
-
### 3.
|
|
2945
|
+
### 3. CREATE TASKS
|
|
2942
2946
|
|
|
2943
2947
|
If you identified useful overnight work:
|
|
2944
|
-
1. **Check board load first.** Count open and awaiting-review tasks. If the human already has 3+ items needing their attention, limit to 1
|
|
2945
|
-
2. Use \`
|
|
2948
|
+
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.
|
|
2949
|
+
2. Use \`deskfree_create_task\` to create 1-3 well-scoped tasks. Quality over quantity.
|
|
2946
2950
|
3. Each task should be self-contained \u2014 it must be completable without human input.
|
|
2947
2951
|
4. Set \`scheduledFor\` if work should start at a specific time (e.g. early morning).
|
|
2948
|
-
5. If nothing genuinely useful can be done overnight, skip
|
|
2952
|
+
5. If nothing genuinely useful can be done overnight, skip task creation entirely. Don't force it.
|
|
2949
2953
|
|
|
2950
2954
|
### 4. BRIEF THE HUMAN
|
|
2951
2955
|
|
|
2952
2956
|
Send a brief main-thread message via \`deskfree_send_message\`:
|
|
2953
2957
|
- 2-3 sentence summary: what happened today + what you're proposing for overnight (if anything).
|
|
2954
2958
|
- Keep it conversational and useful \u2014 this is the human's "end of day" touchpoint.
|
|
2955
|
-
- If no
|
|
2959
|
+
- If no tasks created, still send a brief day summary.
|
|
2956
2960
|
|
|
2957
2961
|
### Rules
|
|
2958
|
-
- Do NOT
|
|
2962
|
+
- Do NOT create tasks for things already on the board or recently completed.
|
|
2959
2963
|
- Do NOT repeat suggestions the human previously ignored or rejected.
|
|
2960
2964
|
- Quality over quantity. One good task beats three mediocre ones.
|
|
2961
2965
|
- Keep the briefing message short and actionable.
|
|
2962
|
-
- Cross-reference memory for recurring patterns \u2014 if something is due,
|
|
2966
|
+
- Cross-reference memory for recurring patterns \u2014 if something is due, create a task for it.
|
|
2963
2967
|
- Use \`deskfree_read_file\` only if you need file content beyond what's in your prompt.`;
|
|
2964
2968
|
}
|
|
2965
2969
|
function setActiveWs(ws) {
|
|
@@ -7097,17 +7101,9 @@ var init_dist = __esm({
|
|
|
7097
7101
|
async consolidateMemory(input) {
|
|
7098
7102
|
return this.request("POST", "memory.consolidate", input);
|
|
7099
7103
|
}
|
|
7100
|
-
/**
|
|
7101
|
-
async
|
|
7102
|
-
|
|
7103
|
-
throw new DeskFreeError(
|
|
7104
|
-
"client",
|
|
7105
|
-
"tasks",
|
|
7106
|
-
"tasks array is required and cannot be empty",
|
|
7107
|
-
"Missing required parameter: tasks."
|
|
7108
|
-
);
|
|
7109
|
-
}
|
|
7110
|
-
return this.request("POST", "tasks.propose", input);
|
|
7104
|
+
/** Create a task directly — no approval needed. */
|
|
7105
|
+
async createTask(input) {
|
|
7106
|
+
return this.request("POST", "tasks.create", input);
|
|
7111
7107
|
}
|
|
7112
7108
|
/**
|
|
7113
7109
|
* Fetch runtime bootstrap config from the backend.
|
|
@@ -7510,63 +7506,49 @@ var init_dist = __esm({
|
|
|
7510
7506
|
)
|
|
7511
7507
|
})
|
|
7512
7508
|
},
|
|
7513
|
-
|
|
7514
|
-
name: "
|
|
7515
|
-
description: "
|
|
7509
|
+
CREATE_TASK: {
|
|
7510
|
+
name: "deskfree_create_task",
|
|
7511
|
+
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.",
|
|
7516
7512
|
parameters: Type.Object({
|
|
7517
|
-
|
|
7513
|
+
title: Type.String({
|
|
7514
|
+
description: "Task title \u2014 short, action-oriented (max 200 chars)"
|
|
7515
|
+
}),
|
|
7516
|
+
instructions: Type.Optional(
|
|
7518
7517
|
Type.String({
|
|
7519
|
-
description: "
|
|
7518
|
+
description: "Detailed instructions, constraints, or context for the task."
|
|
7520
7519
|
})
|
|
7521
7520
|
),
|
|
7522
|
-
|
|
7523
|
-
Type.
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
}),
|
|
7527
|
-
instructions: Type.Optional(
|
|
7528
|
-
Type.String({
|
|
7529
|
-
description: "Detailed instructions, constraints, or context for the task."
|
|
7530
|
-
})
|
|
7531
|
-
),
|
|
7532
|
-
estimatedTokens: Type.Optional(
|
|
7533
|
-
Type.Number({
|
|
7534
|
-
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7535
|
-
})
|
|
7536
|
-
),
|
|
7537
|
-
scheduledFor: Type.Optional(
|
|
7538
|
-
Type.String({
|
|
7539
|
-
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7540
|
-
})
|
|
7541
|
-
),
|
|
7542
|
-
inputFileIds: Type.Optional(
|
|
7543
|
-
Type.Array(
|
|
7544
|
-
Type.String({ description: "File ID to pre-load as context" }),
|
|
7545
|
-
{
|
|
7546
|
-
description: "File IDs to read as context input for this task",
|
|
7547
|
-
maxItems: 20
|
|
7548
|
-
}
|
|
7549
|
-
)
|
|
7550
|
-
),
|
|
7551
|
-
outputFileIds: Type.Optional(
|
|
7552
|
-
Type.Array(
|
|
7553
|
-
Type.String({ description: "File ID to update as deliverable" }),
|
|
7554
|
-
{
|
|
7555
|
-
description: "File IDs this task will produce or update",
|
|
7556
|
-
maxItems: 10
|
|
7557
|
-
}
|
|
7558
|
-
)
|
|
7559
|
-
)
|
|
7560
|
-
}),
|
|
7561
|
-
{
|
|
7562
|
-
description: "Array of tasks to propose (1-20)",
|
|
7563
|
-
minItems: 1,
|
|
7564
|
-
maxItems: 20
|
|
7565
|
-
}
|
|
7521
|
+
estimatedTokens: Type.Optional(
|
|
7522
|
+
Type.Number({
|
|
7523
|
+
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7524
|
+
})
|
|
7566
7525
|
),
|
|
7567
|
-
|
|
7526
|
+
scheduledFor: Type.Optional(
|
|
7568
7527
|
Type.String({
|
|
7569
|
-
description: "
|
|
7528
|
+
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7529
|
+
})
|
|
7530
|
+
),
|
|
7531
|
+
inputFileIds: Type.Optional(
|
|
7532
|
+
Type.Array(
|
|
7533
|
+
Type.String({ description: "File ID to pre-load as context" }),
|
|
7534
|
+
{
|
|
7535
|
+
description: "File IDs to read as context input for this task",
|
|
7536
|
+
maxItems: 20
|
|
7537
|
+
}
|
|
7538
|
+
)
|
|
7539
|
+
),
|
|
7540
|
+
outputFileIds: Type.Optional(
|
|
7541
|
+
Type.Array(
|
|
7542
|
+
Type.String({ description: "File ID to update as deliverable" }),
|
|
7543
|
+
{
|
|
7544
|
+
description: "File IDs this task will produce or update",
|
|
7545
|
+
maxItems: 10
|
|
7546
|
+
}
|
|
7547
|
+
)
|
|
7548
|
+
),
|
|
7549
|
+
suggestedByTaskId: Type.Optional(
|
|
7550
|
+
Type.String({
|
|
7551
|
+
description: "Parent task ID (for follow-up tasks created from within a task thread)"
|
|
7570
7552
|
})
|
|
7571
7553
|
)
|
|
7572
7554
|
})
|
|
@@ -7645,63 +7627,49 @@ var init_dist = __esm({
|
|
|
7645
7627
|
)
|
|
7646
7628
|
})
|
|
7647
7629
|
},
|
|
7648
|
-
|
|
7649
|
-
name: "
|
|
7650
|
-
description: "
|
|
7630
|
+
CREATE_TASK: {
|
|
7631
|
+
name: "deskfree_create_task",
|
|
7632
|
+
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.",
|
|
7651
7633
|
parameters: Type.Object({
|
|
7652
|
-
|
|
7634
|
+
title: Type.String({
|
|
7635
|
+
description: "Task title \u2014 short, action-oriented (max 200 chars)"
|
|
7636
|
+
}),
|
|
7637
|
+
instructions: Type.Optional(
|
|
7653
7638
|
Type.String({
|
|
7654
|
-
description: "
|
|
7639
|
+
description: "Detailed instructions, constraints, or context for the task."
|
|
7655
7640
|
})
|
|
7656
7641
|
),
|
|
7657
|
-
|
|
7658
|
-
Type.
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
}),
|
|
7662
|
-
instructions: Type.Optional(
|
|
7663
|
-
Type.String({
|
|
7664
|
-
description: "Detailed instructions, constraints, or context for the task."
|
|
7665
|
-
})
|
|
7666
|
-
),
|
|
7667
|
-
estimatedTokens: Type.Optional(
|
|
7668
|
-
Type.Number({
|
|
7669
|
-
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7670
|
-
})
|
|
7671
|
-
),
|
|
7672
|
-
scheduledFor: Type.Optional(
|
|
7673
|
-
Type.String({
|
|
7674
|
-
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7675
|
-
})
|
|
7676
|
-
),
|
|
7677
|
-
inputFileIds: Type.Optional(
|
|
7678
|
-
Type.Array(
|
|
7679
|
-
Type.String({ description: "File ID to pre-load as context" }),
|
|
7680
|
-
{
|
|
7681
|
-
description: "File IDs to read as context input for this task",
|
|
7682
|
-
maxItems: 20
|
|
7683
|
-
}
|
|
7684
|
-
)
|
|
7685
|
-
),
|
|
7686
|
-
outputFileIds: Type.Optional(
|
|
7687
|
-
Type.Array(
|
|
7688
|
-
Type.String({ description: "File ID to update as deliverable" }),
|
|
7689
|
-
{
|
|
7690
|
-
description: "File IDs this task will produce or update",
|
|
7691
|
-
maxItems: 10
|
|
7692
|
-
}
|
|
7693
|
-
)
|
|
7694
|
-
)
|
|
7695
|
-
}),
|
|
7696
|
-
{
|
|
7697
|
-
description: "Array of tasks to propose (1-20)",
|
|
7698
|
-
minItems: 1,
|
|
7699
|
-
maxItems: 20
|
|
7700
|
-
}
|
|
7642
|
+
estimatedTokens: Type.Optional(
|
|
7643
|
+
Type.Number({
|
|
7644
|
+
description: "Estimated token cost \u2014 consider files to read, reasoning, output"
|
|
7645
|
+
})
|
|
7701
7646
|
),
|
|
7702
|
-
|
|
7647
|
+
scheduledFor: Type.Optional(
|
|
7703
7648
|
Type.String({
|
|
7704
|
-
description: "
|
|
7649
|
+
description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
|
|
7650
|
+
})
|
|
7651
|
+
),
|
|
7652
|
+
inputFileIds: Type.Optional(
|
|
7653
|
+
Type.Array(
|
|
7654
|
+
Type.String({ description: "File ID to pre-load as context" }),
|
|
7655
|
+
{
|
|
7656
|
+
description: "File IDs to read as context input for this task",
|
|
7657
|
+
maxItems: 20
|
|
7658
|
+
}
|
|
7659
|
+
)
|
|
7660
|
+
),
|
|
7661
|
+
outputFileIds: Type.Optional(
|
|
7662
|
+
Type.Array(
|
|
7663
|
+
Type.String({ description: "File ID to update as deliverable" }),
|
|
7664
|
+
{
|
|
7665
|
+
description: "File IDs this task will produce or update",
|
|
7666
|
+
maxItems: 10
|
|
7667
|
+
}
|
|
7668
|
+
)
|
|
7669
|
+
),
|
|
7670
|
+
suggestedByTaskId: Type.Optional(
|
|
7671
|
+
Type.String({
|
|
7672
|
+
description: "Parent task ID (for follow-up tasks created from within a task thread)"
|
|
7705
7673
|
})
|
|
7706
7674
|
)
|
|
7707
7675
|
})
|
|
@@ -7798,32 +7766,32 @@ var init_dist = __esm({
|
|
|
7798
7766
|
})
|
|
7799
7767
|
},
|
|
7800
7768
|
SEND_MESSAGE: SHARED_TOOLS.SEND_MESSAGE,
|
|
7801
|
-
|
|
7769
|
+
CREATE_TASK: SHARED_TOOLS.CREATE_TASK
|
|
7802
7770
|
};
|
|
7803
7771
|
MAX_FULL_MESSAGES = 15;
|
|
7804
7772
|
DESKFREE_AGENT_DIRECTIVE = `## DeskFree \u2014 Main Thread
|
|
7805
|
-
You handle the main conversation thread. Your job: turn human intent into
|
|
7773
|
+
You handle the main conversation thread. Your job: turn human intent into concrete tasks, then start working on them.
|
|
7806
7774
|
|
|
7807
|
-
**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
|
|
7775
|
+
**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.
|
|
7808
7776
|
|
|
7809
|
-
**The core loop
|
|
7777
|
+
**The core loop:**
|
|
7810
7778
|
|
|
7811
7779
|
1. **Check state** \u2192 \`deskfree_state\` \u2014 see tasks and files. Use \`deskfree_orient\` to recall relevant memories.
|
|
7812
|
-
2. **
|
|
7813
|
-
3. **Start work** \u2192 \`deskfree_dispatch_worker\` with the taskId
|
|
7780
|
+
2. **Create tasks** \u2192 \`deskfree_create_task\` \u2014 turn requests into concrete tasks.
|
|
7781
|
+
3. **Start work** \u2192 \`deskfree_dispatch_worker\` with the taskId to work on the task in a thread.
|
|
7814
7782
|
4. **Communicate** \u2192 \`deskfree_send_message\` for updates outside task threads.
|
|
7815
7783
|
|
|
7816
|
-
**Before
|
|
7817
|
-
- **One-off task** ("proofread this") \u2192
|
|
7818
|
-
- **New aspiration** ("I want to start posting on LinkedIn") \u2192 don't rush to
|
|
7819
|
-
- Never call \`
|
|
7784
|
+
**Before creating a task, qualify the request.** Figure out what kind of thing this is:
|
|
7785
|
+
- **One-off task** ("proofread this") \u2192 create a task directly.
|
|
7786
|
+
- **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.
|
|
7787
|
+
- Never call \`deskfree_create_task\` as your very first action \u2014 qualify first, even if briefly.
|
|
7820
7788
|
|
|
7821
7789
|
**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.
|
|
7822
7790
|
|
|
7823
|
-
In the main thread you
|
|
7791
|
+
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.
|
|
7824
7792
|
- When a human writes in a task thread, decide:
|
|
7825
7793
|
- **Continuation of the same task?** \u2192 reopen and pick it back up.
|
|
7826
|
-
- **New/different work request?** \u2192
|
|
7794
|
+
- **New/different work request?** \u2192 create it as a new task (don't reopen the old one).
|
|
7827
7795
|
- **Just confirmation or deferred?** \u2192 leave it for now.
|
|
7828
7796
|
- Estimate token cost per task \u2014 consider files to read, reasoning, output.
|
|
7829
7797
|
|
|
@@ -7843,7 +7811,7 @@ Record immediately when: the human corrects you, expresses a preference, shares
|
|
|
7843
7811
|
DESKFREE_WORKER_DIRECTIVE = `## DeskFree \u2014 Task Thread
|
|
7844
7812
|
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.
|
|
7845
7813
|
|
|
7846
|
-
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,
|
|
7814
|
+
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.
|
|
7847
7815
|
|
|
7848
7816
|
**Context loading:**
|
|
7849
7817
|
- If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
|
|
@@ -7854,24 +7822,30 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
|
|
|
7854
7822
|
- If no pre-loaded context (edge case/fallback), call \`deskfree_start_task\` with your taskId to load it.
|
|
7855
7823
|
- 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.
|
|
7856
7824
|
|
|
7825
|
+
**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).
|
|
7826
|
+
|
|
7857
7827
|
**Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
|
|
7858
7828
|
|
|
7859
7829
|
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.
|
|
7860
|
-
2. **Align** \u2014
|
|
7830
|
+
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\`.
|
|
7861
7831
|
- **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.
|
|
7862
7832
|
- **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
|
|
7863
7833
|
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.
|
|
7864
|
-
4. **Deliver** \u2014 When work is ready for review,
|
|
7834
|
+
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.
|
|
7865
7835
|
|
|
7866
7836
|
**Push back when warranted:**
|
|
7867
7837
|
- 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.
|
|
7868
7838
|
- 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.
|
|
7869
7839
|
- You're a teammate, not a task executor. Have an opinion when you have the context to form one.
|
|
7870
7840
|
|
|
7841
|
+
**New requests mid-task:**
|
|
7842
|
+
- 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.
|
|
7843
|
+
- 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.
|
|
7844
|
+
- If you notice follow-up work yourself while executing, create a task immediately \u2014 don't wait until completion.
|
|
7845
|
+
|
|
7871
7846
|
**File rules:**
|
|
7872
7847
|
- Create files when your task naturally produces them. Don't be afraid to create multiple files if the work calls for it.
|
|
7873
7848
|
- Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
|
|
7874
|
-
- 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.
|
|
7875
7849
|
|
|
7876
7850
|
**Learnings \u2014 record aggressively:**
|
|
7877
7851
|
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.
|
|
@@ -11914,14 +11888,23 @@ var require_websocket_server2 = __commonJS({
|
|
|
11914
11888
|
});
|
|
11915
11889
|
|
|
11916
11890
|
// ../../node_modules/ws/wrapper.mjs
|
|
11917
|
-
var
|
|
11891
|
+
var wrapper_exports = {};
|
|
11892
|
+
__export(wrapper_exports, {
|
|
11893
|
+
Receiver: () => import_receiver2.default,
|
|
11894
|
+
Sender: () => import_sender2.default,
|
|
11895
|
+
WebSocket: () => import_websocket2.default,
|
|
11896
|
+
WebSocketServer: () => import_websocket_server2.default,
|
|
11897
|
+
createWebSocketStream: () => import_stream2.default,
|
|
11898
|
+
default: () => wrapper_default2
|
|
11899
|
+
});
|
|
11900
|
+
var import_stream2, import_receiver2, import_sender2, import_websocket2, import_websocket_server2, wrapper_default2;
|
|
11918
11901
|
var init_wrapper = __esm({
|
|
11919
11902
|
"../../node_modules/ws/wrapper.mjs"() {
|
|
11920
|
-
__toESM(require_stream2());
|
|
11921
|
-
__toESM(require_receiver2());
|
|
11922
|
-
__toESM(require_sender2());
|
|
11903
|
+
import_stream2 = __toESM(require_stream2());
|
|
11904
|
+
import_receiver2 = __toESM(require_receiver2());
|
|
11905
|
+
import_sender2 = __toESM(require_sender2());
|
|
11923
11906
|
import_websocket2 = __toESM(require_websocket2());
|
|
11924
|
-
__toESM(require_websocket_server2());
|
|
11907
|
+
import_websocket_server2 = __toESM(require_websocket_server2());
|
|
11925
11908
|
wrapper_default2 = import_websocket2.default;
|
|
11926
11909
|
}
|
|
11927
11910
|
});
|
|
@@ -11935,9 +11918,7 @@ __export(ws_gateway_exports, {
|
|
|
11935
11918
|
});
|
|
11936
11919
|
function getRotationToken() {
|
|
11937
11920
|
if (!currentRotationToken) {
|
|
11938
|
-
throw new Error(
|
|
11939
|
-
"No rotation token available \u2014 bots.connect not yet called"
|
|
11940
|
-
);
|
|
11921
|
+
throw new Error("No rotation token available \u2014 WS init not yet completed");
|
|
11941
11922
|
}
|
|
11942
11923
|
return currentRotationToken;
|
|
11943
11924
|
}
|
|
@@ -11969,54 +11950,88 @@ function sleepWithAbort(ms, signal) {
|
|
|
11969
11950
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
11970
11951
|
});
|
|
11971
11952
|
}
|
|
11972
|
-
async function
|
|
11973
|
-
const { botId,
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
}
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
method: "POST",
|
|
11990
|
-
headers: { "Content-Type": "application/json" },
|
|
11991
|
-
body: JSON.stringify(body)
|
|
11992
|
-
});
|
|
11993
|
-
if (!response.ok) {
|
|
11994
|
-
const text = await response.text().catch(() => "");
|
|
11995
|
-
throw new Error(`bots.connect failed: ${response.status} ${text}`);
|
|
11996
|
-
}
|
|
11997
|
-
const json = await response.json();
|
|
11998
|
-
const data = json.result?.data;
|
|
11999
|
-
if (!data) throw new Error("bots.connect: invalid response structure");
|
|
12000
|
-
if (data.status === "approved") {
|
|
12001
|
-
return {
|
|
12002
|
-
ticket: data.ticket,
|
|
12003
|
-
wsUrl: data.wsUrl,
|
|
12004
|
-
rotationToken: data.rotationToken
|
|
11953
|
+
async function wsReconnect(config, rotationToken) {
|
|
11954
|
+
const { botId, stage, wsUrl, log, abortSignal } = config;
|
|
11955
|
+
const params = new URLSearchParams({ id: botId, stage });
|
|
11956
|
+
if (rotationToken) {
|
|
11957
|
+
params.set("token", rotationToken);
|
|
11958
|
+
}
|
|
11959
|
+
const fullUrl = `${wsUrl}?${params.toString()}`;
|
|
11960
|
+
return new Promise(
|
|
11961
|
+
(resolve, reject) => {
|
|
11962
|
+
const ws = new wrapper_default2(fullUrl);
|
|
11963
|
+
let settled = false;
|
|
11964
|
+
let timeoutTimer;
|
|
11965
|
+
const cleanup = () => {
|
|
11966
|
+
if (timeoutTimer !== void 0) {
|
|
11967
|
+
clearTimeout(timeoutTimer);
|
|
11968
|
+
timeoutTimer = void 0;
|
|
11969
|
+
}
|
|
12005
11970
|
};
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
11971
|
+
const fail = (err) => {
|
|
11972
|
+
if (settled) return;
|
|
11973
|
+
settled = true;
|
|
11974
|
+
cleanup();
|
|
11975
|
+
try {
|
|
11976
|
+
ws.close();
|
|
11977
|
+
} catch {
|
|
11978
|
+
}
|
|
11979
|
+
reject(err);
|
|
11980
|
+
};
|
|
11981
|
+
timeoutTimer = setTimeout(() => {
|
|
11982
|
+
fail(
|
|
11983
|
+
new Error(
|
|
11984
|
+
`WS reconnect timeout after ${WS_RECONNECT_INIT_TIMEOUT_MS}ms`
|
|
11985
|
+
)
|
|
11986
|
+
);
|
|
11987
|
+
}, WS_RECONNECT_INIT_TIMEOUT_MS);
|
|
11988
|
+
ws.on("open", () => {
|
|
11989
|
+
ws.send(JSON.stringify({ action: "init" }));
|
|
11990
|
+
});
|
|
11991
|
+
ws.on("message", (data) => {
|
|
11992
|
+
try {
|
|
11993
|
+
const msg = JSON.parse(data.toString());
|
|
11994
|
+
if (msg.action === "go" && msg.rotationToken) {
|
|
11995
|
+
if (settled) return;
|
|
11996
|
+
settled = true;
|
|
11997
|
+
cleanup();
|
|
11998
|
+
resolve({ ws, rotationToken: msg.rotationToken });
|
|
11999
|
+
} else if (msg.action === "rejected") {
|
|
12000
|
+
fail(
|
|
12001
|
+
new Error(
|
|
12002
|
+
`Reconnect rejected: ${msg.reason ?? "unknown reason"}`
|
|
12003
|
+
)
|
|
12004
|
+
);
|
|
12005
|
+
} else if (msg.action === "lobby") {
|
|
12006
|
+
log.info("Reconnect landed in lobby \u2014 awaiting approval...");
|
|
12007
|
+
cleanup();
|
|
12008
|
+
}
|
|
12009
|
+
} catch (err) {
|
|
12010
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12011
|
+
log.warn(`Error parsing reconnect response: ${errMsg}`);
|
|
12012
|
+
}
|
|
12013
|
+
});
|
|
12014
|
+
ws.on("error", (err) => {
|
|
12015
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
12016
|
+
});
|
|
12017
|
+
ws.on("close", (code, reason) => {
|
|
12018
|
+
if (!settled) {
|
|
12019
|
+
fail(
|
|
12020
|
+
new Error(
|
|
12021
|
+
`WebSocket closed during reconnect: ${code} ${reason.toString()}`
|
|
12022
|
+
)
|
|
12023
|
+
);
|
|
12024
|
+
}
|
|
12025
|
+
});
|
|
12026
|
+
abortSignal.addEventListener(
|
|
12027
|
+
"abort",
|
|
12028
|
+
() => {
|
|
12029
|
+
fail(new Error("Aborted during WS reconnect"));
|
|
12030
|
+
},
|
|
12031
|
+
{ once: true }
|
|
12016
12032
|
);
|
|
12017
12033
|
}
|
|
12018
|
-
|
|
12019
|
-
}
|
|
12034
|
+
);
|
|
12020
12035
|
}
|
|
12021
12036
|
async function startGateway(config) {
|
|
12022
12037
|
const { client, accountId, stateDir, log, abortSignal } = config;
|
|
@@ -12034,33 +12049,24 @@ async function startGateway(config) {
|
|
|
12034
12049
|
});
|
|
12035
12050
|
while (!abortSignal.aborted) {
|
|
12036
12051
|
try {
|
|
12037
|
-
let
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12052
|
+
let ws;
|
|
12053
|
+
if (totalReconnects === 0) {
|
|
12054
|
+
ws = config.initialWs;
|
|
12055
|
+
currentRotationToken = config.initialRotationToken;
|
|
12056
|
+
log.info("Using initial WS connection from handshake.");
|
|
12042
12057
|
} else {
|
|
12043
|
-
const result = await callBotsConnect(
|
|
12044
|
-
config,
|
|
12045
|
-
currentRotationToken ?? void 0
|
|
12046
|
-
);
|
|
12047
|
-
ticket = result.ticket;
|
|
12048
|
-
wsUrl = result.wsUrl;
|
|
12049
|
-
currentRotationToken = result.rotationToken;
|
|
12050
|
-
}
|
|
12051
|
-
resetBackoff(backoff);
|
|
12052
|
-
if (totalReconnects > 0) {
|
|
12053
12058
|
log.info(
|
|
12054
|
-
`
|
|
12059
|
+
`Reconnecting to ${config.wsUrl}... (reconnect #${totalReconnects})`
|
|
12055
12060
|
);
|
|
12061
|
+
const result = await wsReconnect(config, currentRotationToken ?? "");
|
|
12062
|
+
ws = result.ws;
|
|
12063
|
+
currentRotationToken = result.rotationToken;
|
|
12056
12064
|
recordReconnect(accountId);
|
|
12057
|
-
} else {
|
|
12058
|
-
log.info(`Got WS ticket, connecting to ${wsUrl}...`);
|
|
12059
12065
|
}
|
|
12066
|
+
resetBackoff(backoff);
|
|
12060
12067
|
updateHealthMode(accountId, "websocket");
|
|
12061
12068
|
cursor = await runWebSocketConnection({
|
|
12062
|
-
|
|
12063
|
-
wsUrl,
|
|
12069
|
+
ws,
|
|
12064
12070
|
client,
|
|
12065
12071
|
accountId,
|
|
12066
12072
|
stateDir,
|
|
@@ -12069,33 +12075,22 @@ async function startGateway(config) {
|
|
|
12069
12075
|
abortSignal,
|
|
12070
12076
|
onMessage: config.onMessage,
|
|
12071
12077
|
getWorkerStatus: config.getWorkerStatus,
|
|
12072
|
-
onConsolidate: config.onConsolidate
|
|
12073
|
-
isV2: true
|
|
12078
|
+
onConsolidate: config.onConsolidate
|
|
12074
12079
|
});
|
|
12075
12080
|
totalReconnects++;
|
|
12076
12081
|
} catch (err) {
|
|
12077
12082
|
totalReconnects++;
|
|
12078
12083
|
const message = err instanceof Error ? err.message : String(err);
|
|
12079
|
-
|
|
12084
|
+
const isConnectFailure = message.includes("WS-first connect") || message.includes("WS reconnect") || message.includes("Connection rejected") || message.includes("Reconnect rejected");
|
|
12085
|
+
if (isConnectFailure) {
|
|
12080
12086
|
log.warn(
|
|
12081
|
-
`Connection setup failed (attempt #${totalReconnects}): ${message}.
|
|
12087
|
+
`Connection setup failed (attempt #${totalReconnects}): ${message}. Will retry after backoff.`
|
|
12082
12088
|
);
|
|
12083
12089
|
reportError("error", `Connection setup failed: ${message}`, {
|
|
12084
12090
|
component: "gateway",
|
|
12085
|
-
event: "
|
|
12091
|
+
event: "ws_connect_failed",
|
|
12086
12092
|
attempt: totalReconnects
|
|
12087
12093
|
});
|
|
12088
|
-
recordReconnect(accountId);
|
|
12089
|
-
updateHealthMode(accountId, "polling");
|
|
12090
|
-
cursor = await runPollingFallback({
|
|
12091
|
-
client,
|
|
12092
|
-
accountId,
|
|
12093
|
-
stateDir,
|
|
12094
|
-
cursor,
|
|
12095
|
-
log,
|
|
12096
|
-
abortSignal,
|
|
12097
|
-
onMessage: config.onMessage
|
|
12098
|
-
});
|
|
12099
12094
|
} else {
|
|
12100
12095
|
log.warn(`Connection error (attempt #${totalReconnects}): ${message}`);
|
|
12101
12096
|
reportError("warn", `WS connection error: ${message}`, {
|
|
@@ -12103,8 +12098,8 @@ async function startGateway(config) {
|
|
|
12103
12098
|
event: "connection_error",
|
|
12104
12099
|
attempt: totalReconnects
|
|
12105
12100
|
});
|
|
12106
|
-
recordReconnect(accountId);
|
|
12107
12101
|
}
|
|
12102
|
+
recordReconnect(accountId);
|
|
12108
12103
|
if (abortSignal.aborted) break;
|
|
12109
12104
|
const delay = nextBackoff(backoff);
|
|
12110
12105
|
log.info(
|
|
@@ -12116,26 +12111,20 @@ async function startGateway(config) {
|
|
|
12116
12111
|
log.info(`Gateway loop exited after ${totalReconnects} reconnect(s).`);
|
|
12117
12112
|
}
|
|
12118
12113
|
async function runWebSocketConnection(opts) {
|
|
12119
|
-
const {
|
|
12114
|
+
const { client, accountId, stateDir, log, abortSignal } = opts;
|
|
12115
|
+
const ws = opts.ws;
|
|
12120
12116
|
const ctx = { accountId };
|
|
12121
12117
|
let cursor = opts.cursor;
|
|
12122
12118
|
return new Promise((resolve, reject) => {
|
|
12123
|
-
const ws = new wrapper_default2(`${wsUrl}?ticket=${ticket}`);
|
|
12124
12119
|
let pingInterval;
|
|
12125
|
-
let connectionTimer;
|
|
12126
12120
|
let pongTimer;
|
|
12127
12121
|
let notifyDebounceTimer;
|
|
12128
12122
|
let proactiveReconnectTimer;
|
|
12129
|
-
let isConnected = false;
|
|
12130
12123
|
const cleanup = () => {
|
|
12131
12124
|
if (pingInterval !== void 0) {
|
|
12132
12125
|
clearInterval(pingInterval);
|
|
12133
12126
|
pingInterval = void 0;
|
|
12134
12127
|
}
|
|
12135
|
-
if (connectionTimer !== void 0) {
|
|
12136
|
-
clearTimeout(connectionTimer);
|
|
12137
|
-
connectionTimer = void 0;
|
|
12138
|
-
}
|
|
12139
12128
|
if (pongTimer !== void 0) {
|
|
12140
12129
|
clearTimeout(pongTimer);
|
|
12141
12130
|
pongTimer = void 0;
|
|
@@ -12149,88 +12138,67 @@ async function runWebSocketConnection(opts) {
|
|
|
12149
12138
|
proactiveReconnectTimer = void 0;
|
|
12150
12139
|
}
|
|
12151
12140
|
};
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12141
|
+
setActiveWs(ws);
|
|
12142
|
+
log.info("WebSocket session started.");
|
|
12143
|
+
setWsConnected(true);
|
|
12144
|
+
setHealthMode("websocket");
|
|
12145
|
+
pingInterval = setInterval(() => {
|
|
12146
|
+
if (ws.readyState === wrapper_default2.OPEN) {
|
|
12155
12147
|
try {
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
reject(
|
|
12160
|
-
new Error(
|
|
12161
|
-
`WebSocket connection timeout after ${WS_CONNECTION_TIMEOUT_MS}ms`
|
|
12162
|
-
)
|
|
12163
|
-
);
|
|
12164
|
-
}
|
|
12165
|
-
}, WS_CONNECTION_TIMEOUT_MS);
|
|
12166
|
-
ws.on("open", () => {
|
|
12167
|
-
isConnected = true;
|
|
12168
|
-
setActiveWs(ws);
|
|
12169
|
-
if (connectionTimer !== void 0) {
|
|
12170
|
-
clearTimeout(connectionTimer);
|
|
12171
|
-
connectionTimer = void 0;
|
|
12172
|
-
}
|
|
12173
|
-
log.info("WebSocket connected.");
|
|
12174
|
-
setWsConnected(true);
|
|
12175
|
-
setHealthMode("websocket");
|
|
12176
|
-
pingInterval = setInterval(() => {
|
|
12177
|
-
if (ws.readyState === wrapper_default2.OPEN) {
|
|
12178
|
-
try {
|
|
12179
|
-
const pingPayload = { action: "ping" };
|
|
12180
|
-
if (currentRotationToken) {
|
|
12181
|
-
pingPayload.token = currentRotationToken;
|
|
12182
|
-
}
|
|
12183
|
-
ws.send(JSON.stringify(pingPayload));
|
|
12184
|
-
pongTimer = setTimeout(() => {
|
|
12185
|
-
log.warn("Pong timeout \u2014 closing WebSocket");
|
|
12186
|
-
try {
|
|
12187
|
-
ws.close(1002, "pong timeout");
|
|
12188
|
-
} catch {
|
|
12189
|
-
}
|
|
12190
|
-
}, WS_PONG_TIMEOUT_MS);
|
|
12191
|
-
} catch (err) {
|
|
12192
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12193
|
-
log.warn(`Failed to send ping: ${msg}`);
|
|
12148
|
+
const pingPayload = { action: "ping" };
|
|
12149
|
+
if (currentRotationToken) {
|
|
12150
|
+
pingPayload.token = currentRotationToken;
|
|
12194
12151
|
}
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
}, PROACTIVE_RECONNECT_MS);
|
|
12204
|
-
if (opts.getWorkerStatus) {
|
|
12205
|
-
try {
|
|
12206
|
-
const status = opts.getWorkerStatus();
|
|
12207
|
-
ws.send(
|
|
12208
|
-
JSON.stringify({
|
|
12209
|
-
action: "heartbeatResponse",
|
|
12210
|
-
...status
|
|
12211
|
-
})
|
|
12212
|
-
);
|
|
12152
|
+
ws.send(JSON.stringify(pingPayload));
|
|
12153
|
+
pongTimer = setTimeout(() => {
|
|
12154
|
+
log.warn("Pong timeout \u2014 closing WebSocket");
|
|
12155
|
+
try {
|
|
12156
|
+
ws.close(1002, "pong timeout");
|
|
12157
|
+
} catch {
|
|
12158
|
+
}
|
|
12159
|
+
}, WS_PONG_TIMEOUT_MS);
|
|
12213
12160
|
} catch (err) {
|
|
12214
|
-
const
|
|
12215
|
-
log.warn(`Failed to send
|
|
12161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12162
|
+
log.warn(`Failed to send ping: ${msg}`);
|
|
12216
12163
|
}
|
|
12217
12164
|
}
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12165
|
+
}, PING_INTERVAL_MS);
|
|
12166
|
+
proactiveReconnectTimer = setTimeout(() => {
|
|
12167
|
+
log.info("Proactive reconnect at 1h50m \u2014 closing for reconnect");
|
|
12168
|
+
try {
|
|
12169
|
+
ws.close(1e3, "proactive_reconnect");
|
|
12170
|
+
} catch {
|
|
12171
|
+
}
|
|
12172
|
+
}, PROACTIVE_RECONNECT_MS);
|
|
12173
|
+
if (opts.getWorkerStatus) {
|
|
12174
|
+
try {
|
|
12175
|
+
const status = opts.getWorkerStatus();
|
|
12176
|
+
ws.send(
|
|
12177
|
+
JSON.stringify({
|
|
12178
|
+
action: "heartbeatResponse",
|
|
12179
|
+
...status
|
|
12180
|
+
})
|
|
12181
|
+
);
|
|
12182
|
+
} catch (err) {
|
|
12183
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12184
|
+
log.warn(`Failed to send initial heartbeat: ${errMsg}`);
|
|
12185
|
+
}
|
|
12186
|
+
}
|
|
12187
|
+
void pollAndDeliver(
|
|
12188
|
+
client,
|
|
12189
|
+
accountId,
|
|
12190
|
+
stateDir,
|
|
12191
|
+
cursor,
|
|
12192
|
+
opts.onMessage,
|
|
12193
|
+
log
|
|
12194
|
+
).then((result) => {
|
|
12195
|
+
if (result.cursor) {
|
|
12196
|
+
cursor = result.cursor;
|
|
12197
|
+
saveCursor(ctx, cursor, stateDir, log);
|
|
12198
|
+
}
|
|
12199
|
+
}).catch((err) => {
|
|
12200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12201
|
+
log.warn(`Initial poll failed: ${msg}`);
|
|
12234
12202
|
});
|
|
12235
12203
|
ws.on("message", (data) => {
|
|
12236
12204
|
try {
|
|
@@ -12278,9 +12246,7 @@ async function runWebSocketConnection(opts) {
|
|
|
12278
12246
|
currentRotationToken = pongMsg.rotationToken;
|
|
12279
12247
|
log.debug("Received pong \u2014 token rotated");
|
|
12280
12248
|
} else if (pongMsg.error === "rotation_invalid") {
|
|
12281
|
-
log.warn(
|
|
12282
|
-
"Rotation token invalid \u2014 closing for reconnect via bots.connect"
|
|
12283
|
-
);
|
|
12249
|
+
log.warn("Rotation token invalid \u2014 closing for reconnect");
|
|
12284
12250
|
try {
|
|
12285
12251
|
ws.close(1e3, "rotation_invalid");
|
|
12286
12252
|
} catch {
|
|
@@ -12389,7 +12355,6 @@ async function runWebSocketConnection(opts) {
|
|
|
12389
12355
|
});
|
|
12390
12356
|
ws.on("close", (code, reason) => {
|
|
12391
12357
|
cleanup();
|
|
12392
|
-
isConnected = false;
|
|
12393
12358
|
setActiveWs(null);
|
|
12394
12359
|
setWsConnected(false);
|
|
12395
12360
|
if (code === 1e3 || code === 1001) {
|
|
@@ -12423,7 +12388,6 @@ async function runWebSocketConnection(opts) {
|
|
|
12423
12388
|
});
|
|
12424
12389
|
ws.on("error", (err) => {
|
|
12425
12390
|
cleanup();
|
|
12426
|
-
isConnected = false;
|
|
12427
12391
|
setActiveWs(null);
|
|
12428
12392
|
setWsConnected(false);
|
|
12429
12393
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -12452,50 +12416,7 @@ async function runWebSocketConnection(opts) {
|
|
|
12452
12416
|
);
|
|
12453
12417
|
});
|
|
12454
12418
|
}
|
|
12455
|
-
|
|
12456
|
-
const { client, accountId, stateDir, log, abortSignal } = opts;
|
|
12457
|
-
let cursor = opts.cursor;
|
|
12458
|
-
let iterations = 0;
|
|
12459
|
-
let consecutiveFailures = 0;
|
|
12460
|
-
log.info("Running in polling fallback mode.");
|
|
12461
|
-
setHealthMode("polling");
|
|
12462
|
-
while (!abortSignal.aborted && iterations < MAX_POLLING_ITERATIONS) {
|
|
12463
|
-
const result = await pollAndDeliver(
|
|
12464
|
-
client,
|
|
12465
|
-
accountId,
|
|
12466
|
-
stateDir,
|
|
12467
|
-
cursor,
|
|
12468
|
-
opts.onMessage,
|
|
12469
|
-
log
|
|
12470
|
-
);
|
|
12471
|
-
if (result.ok) {
|
|
12472
|
-
if (result.cursor) cursor = result.cursor;
|
|
12473
|
-
consecutiveFailures = 0;
|
|
12474
|
-
} else {
|
|
12475
|
-
consecutiveFailures++;
|
|
12476
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
12477
|
-
log.warn(
|
|
12478
|
-
`${consecutiveFailures} consecutive poll failures, breaking to retry WebSocket`
|
|
12479
|
-
);
|
|
12480
|
-
reportError(
|
|
12481
|
-
"error",
|
|
12482
|
-
`${consecutiveFailures} consecutive poll failures, switching to WS retry`,
|
|
12483
|
-
{
|
|
12484
|
-
component: "gateway",
|
|
12485
|
-
event: "poll_max_failures",
|
|
12486
|
-
consecutiveFailures
|
|
12487
|
-
}
|
|
12488
|
-
);
|
|
12489
|
-
break;
|
|
12490
|
-
}
|
|
12491
|
-
}
|
|
12492
|
-
iterations++;
|
|
12493
|
-
const jitter = Math.random() * POLL_FALLBACK_INTERVAL_MS * 0.2;
|
|
12494
|
-
await sleepWithAbort(POLL_FALLBACK_INTERVAL_MS + jitter, abortSignal);
|
|
12495
|
-
}
|
|
12496
|
-
return cursor;
|
|
12497
|
-
}
|
|
12498
|
-
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, CONNECT_POLL_INTERVAL_MS, PROACTIVE_RECONNECT_MS, BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_FACTOR, currentRotationToken;
|
|
12419
|
+
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;
|
|
12499
12420
|
var init_ws_gateway = __esm({
|
|
12500
12421
|
"src/gateway/ws-gateway.ts"() {
|
|
12501
12422
|
init_health_state();
|
|
@@ -12504,15 +12425,11 @@ var init_ws_gateway = __esm({
|
|
|
12504
12425
|
init_dist();
|
|
12505
12426
|
init_wrapper();
|
|
12506
12427
|
PING_INTERVAL_MS = 5 * 60 * 1e3;
|
|
12507
|
-
POLL_FALLBACK_INTERVAL_MS = 3e4;
|
|
12508
|
-
WS_CONNECTION_TIMEOUT_MS = 3e4;
|
|
12509
12428
|
WS_PONG_TIMEOUT_MS = 1e4;
|
|
12510
12429
|
NOTIFY_DEBOUNCE_MS = 200;
|
|
12511
12430
|
HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
|
|
12512
|
-
MAX_POLLING_ITERATIONS = 10;
|
|
12513
|
-
MAX_CONSECUTIVE_POLL_FAILURES = 5;
|
|
12514
|
-
CONNECT_POLL_INTERVAL_MS = 3e4;
|
|
12515
12431
|
PROACTIVE_RECONNECT_MS = 110 * 60 * 1e3;
|
|
12432
|
+
WS_RECONNECT_INIT_TIMEOUT_MS = 3e4;
|
|
12516
12433
|
BACKOFF_INITIAL_MS = 2e3;
|
|
12517
12434
|
BACKOFF_MAX_MS = 3e4;
|
|
12518
12435
|
BACKOFF_FACTOR = 1.8;
|
|
@@ -12699,6 +12616,10 @@ function deriveApiUrl() {
|
|
|
12699
12616
|
const stage = process.env["STAGE"] ?? "dev";
|
|
12700
12617
|
return `https://${getStageDomain(stage, domain)}/v1/bot`;
|
|
12701
12618
|
}
|
|
12619
|
+
function deriveWsUrl() {
|
|
12620
|
+
const domain = process.env["DESKFREE_DOMAIN"] ?? "dev.deskfree.ai";
|
|
12621
|
+
return `wss://ws.${domain}`;
|
|
12622
|
+
}
|
|
12702
12623
|
function loadConfig() {
|
|
12703
12624
|
const botId = process.env["BOT"] ?? process.env["DESKFREE_BOT_ID"];
|
|
12704
12625
|
if (!botId) {
|
|
@@ -12725,9 +12646,13 @@ function loadConfig() {
|
|
|
12725
12646
|
if (isNaN(healthPort) || healthPort < 1 || healthPort > 65535) {
|
|
12726
12647
|
throw new Error(`Invalid HEALTH_PORT: ${healthPortRaw}`);
|
|
12727
12648
|
}
|
|
12649
|
+
const stage = process.env["STAGE"] ?? "dev";
|
|
12650
|
+
const wsUrl = process.env["DESKFREE_WS_URL"] ?? deriveWsUrl();
|
|
12728
12651
|
return {
|
|
12729
12652
|
botId,
|
|
12730
12653
|
apiUrl,
|
|
12654
|
+
stage,
|
|
12655
|
+
wsUrl,
|
|
12731
12656
|
stateDir: process.env["DESKFREE_STATE_DIR"] ?? DEFAULTS.stateDir,
|
|
12732
12657
|
toolsDir: process.env["DESKFREE_TOOLS_DIR"] ?? DEFAULTS.toolsDir,
|
|
12733
12658
|
logLevel,
|
|
@@ -12750,7 +12675,7 @@ function mergeWithRemoteConfig(local, remote) {
|
|
|
12750
12675
|
return {
|
|
12751
12676
|
...local,
|
|
12752
12677
|
claudeCodePath,
|
|
12753
|
-
wsUrl: process.env["DESKFREE_WS_URL"] ?? remote.wsUrl,
|
|
12678
|
+
wsUrl: process.env["DESKFREE_WS_URL"] ?? remote.wsUrl ?? local.wsUrl,
|
|
12754
12679
|
model: process.env["DESKFREE_MODEL"] ?? remote.model,
|
|
12755
12680
|
awsRegion: process.env["AWS_REGION"] ?? remote.awsRegion,
|
|
12756
12681
|
heartbeatIntervalMs: process.env["DESKFREE_HEARTBEAT_INTERVAL_MS"] ? parseInt(process.env["DESKFREE_HEARTBEAT_INTERVAL_MS"], 10) : remote.heartbeatIntervalMs,
|
|
@@ -14270,6 +14195,17 @@ ${userMessage}
|
|
|
14270
14195
|
} catch {
|
|
14271
14196
|
}
|
|
14272
14197
|
} finally {
|
|
14198
|
+
try {
|
|
14199
|
+
const task = await client.getTask({ taskId });
|
|
14200
|
+
if (task.status === "open" && task.awaiting === "bot") {
|
|
14201
|
+
log.info(
|
|
14202
|
+
`Worker finished but task ${taskId} still awaiting bot \u2014 auto-flipping to human`
|
|
14203
|
+
);
|
|
14204
|
+
await client.updateTaskStatus({ taskId, awaiting: "human" });
|
|
14205
|
+
}
|
|
14206
|
+
} catch {
|
|
14207
|
+
log.warn(`Failed to check/flip awaiting state for task ${taskId}`);
|
|
14208
|
+
}
|
|
14273
14209
|
const handle = this.workers.get(taskId);
|
|
14274
14210
|
if (handle) {
|
|
14275
14211
|
clearTimeout(handle.idleTimer);
|
|
@@ -14395,51 +14331,84 @@ function scheduleHeartbeat(createOrchServer, model, intervalMs, signal, log, cla
|
|
|
14395
14331
|
setTimeout(() => void tick(), intervalMs);
|
|
14396
14332
|
log.info(`Heartbeat scheduled every ${Math.round(intervalMs / 1e3)}s`);
|
|
14397
14333
|
}
|
|
14398
|
-
var
|
|
14334
|
+
var WS_INIT_TIMEOUT_MS = 3e4;
|
|
14399
14335
|
async function initialConnect(opts) {
|
|
14400
|
-
const { botId,
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14336
|
+
const { botId, stage, wsUrl, fingerprint, log, abortSignal } = opts;
|
|
14337
|
+
const WebSocket2 = (await Promise.resolve().then(() => (init_wrapper(), wrapper_exports))).default;
|
|
14338
|
+
const params = new URLSearchParams({ id: botId, stage });
|
|
14339
|
+
const fullUrl = `${wsUrl}?${params.toString()}`;
|
|
14340
|
+
return new Promise((resolve, reject) => {
|
|
14341
|
+
const ws = new WebSocket2(fullUrl);
|
|
14342
|
+
let settled = false;
|
|
14343
|
+
let timeoutTimer;
|
|
14344
|
+
const cleanup = () => {
|
|
14345
|
+
if (timeoutTimer !== void 0) {
|
|
14346
|
+
clearTimeout(timeoutTimer);
|
|
14347
|
+
timeoutTimer = void 0;
|
|
14348
|
+
}
|
|
14408
14349
|
};
|
|
14409
|
-
const
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14350
|
+
const fail = (err) => {
|
|
14351
|
+
if (settled) return;
|
|
14352
|
+
settled = true;
|
|
14353
|
+
cleanup();
|
|
14354
|
+
try {
|
|
14355
|
+
ws.close();
|
|
14356
|
+
} catch {
|
|
14357
|
+
}
|
|
14358
|
+
reject(err);
|
|
14359
|
+
};
|
|
14360
|
+
timeoutTimer = setTimeout(() => {
|
|
14361
|
+
fail(new Error(`WS-first connect timeout after ${WS_INIT_TIMEOUT_MS}ms`));
|
|
14362
|
+
}, WS_INIT_TIMEOUT_MS);
|
|
14363
|
+
ws.on("open", () => {
|
|
14364
|
+
const initMsg = { action: "init" };
|
|
14365
|
+
{
|
|
14366
|
+
initMsg.fingerprint = fingerprint;
|
|
14367
|
+
}
|
|
14368
|
+
ws.send(JSON.stringify(initMsg));
|
|
14413
14369
|
});
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
}
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14370
|
+
ws.on("message", (data) => {
|
|
14371
|
+
try {
|
|
14372
|
+
const msg = JSON.parse(data.toString());
|
|
14373
|
+
if (msg.action === "go" && msg.rotationToken) {
|
|
14374
|
+
if (settled) return;
|
|
14375
|
+
settled = true;
|
|
14376
|
+
cleanup();
|
|
14377
|
+
ws.removeAllListeners();
|
|
14378
|
+
resolve({ ws, rotationToken: msg.rotationToken });
|
|
14379
|
+
} else if (msg.action === "lobby") {
|
|
14380
|
+
log.info("Awaiting human approval...");
|
|
14381
|
+
cleanup();
|
|
14382
|
+
} else if (msg.action === "rejected") {
|
|
14383
|
+
fail(
|
|
14384
|
+
new Error(`Connection rejected: ${msg.reason ?? "unknown reason"}`)
|
|
14385
|
+
);
|
|
14386
|
+
}
|
|
14387
|
+
} catch (err) {
|
|
14388
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
14389
|
+
log.warn(`Error parsing init response: ${errMsg}`);
|
|
14390
|
+
}
|
|
14391
|
+
});
|
|
14392
|
+
ws.on("error", (err) => {
|
|
14393
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
14394
|
+
});
|
|
14395
|
+
ws.on("close", (code, reason) => {
|
|
14396
|
+
if (!settled) {
|
|
14397
|
+
fail(
|
|
14398
|
+
new Error(
|
|
14399
|
+
`WebSocket closed during init: ${code} ${reason.toString()}`
|
|
14400
|
+
)
|
|
14401
|
+
);
|
|
14402
|
+
}
|
|
14403
|
+
});
|
|
14404
|
+
abortSignal.addEventListener(
|
|
14405
|
+
"abort",
|
|
14406
|
+
() => {
|
|
14407
|
+
fail(new Error("Aborted during WS-first connect"));
|
|
14408
|
+
},
|
|
14409
|
+
{ once: true }
|
|
14410
|
+
);
|
|
14411
|
+
});
|
|
14443
14412
|
}
|
|
14444
14413
|
async function startAgent(opts) {
|
|
14445
14414
|
const localConfig = loadConfig();
|
|
@@ -14448,19 +14417,22 @@ async function startAgent(opts) {
|
|
|
14448
14417
|
log.info("DeskFree Agent Runtime starting...");
|
|
14449
14418
|
const { getRotationToken: getRotationToken2, setInitialRotationToken: setInitialRotationToken2 } = await Promise.resolve().then(() => (init_ws_gateway(), ws_gateway_exports));
|
|
14450
14419
|
const { collectFingerprint: collectFingerprint2 } = await Promise.resolve().then(() => (init_fingerprint(), fingerprint_exports));
|
|
14451
|
-
const
|
|
14452
|
-
const
|
|
14420
|
+
const { createRequire } = await import('module');
|
|
14421
|
+
const require3 = createRequire(import.meta.url);
|
|
14422
|
+
const runtimePkg = require3("../../package.json");
|
|
14423
|
+
const runtimeVersion = runtimePkg.version;
|
|
14453
14424
|
const fingerprint = collectFingerprint2(localConfig.stateDir, runtimeVersion);
|
|
14454
|
-
log.info("Connecting to DeskFree...", {
|
|
14425
|
+
log.info("Connecting to DeskFree...", { wsUrl: localConfig.wsUrl });
|
|
14455
14426
|
const connectResult = await initialConnect({
|
|
14456
14427
|
botId: localConfig.botId,
|
|
14457
|
-
|
|
14428
|
+
stage: localConfig.stage,
|
|
14429
|
+
wsUrl: localConfig.wsUrl,
|
|
14458
14430
|
fingerprint,
|
|
14459
14431
|
log,
|
|
14460
14432
|
abortSignal: abortController.signal
|
|
14461
14433
|
});
|
|
14462
14434
|
setInitialRotationToken2(connectResult.rotationToken);
|
|
14463
|
-
log.info("Connected \u2014 got rotation token
|
|
14435
|
+
log.info("Connected \u2014 got rotation token via WS handshake.");
|
|
14464
14436
|
const client = new DeskFreeClient({
|
|
14465
14437
|
apiUrl: localConfig.apiUrl,
|
|
14466
14438
|
getToken: () => getRotationToken2()
|
|
@@ -14555,7 +14527,7 @@ async function startAgent(opts) {
|
|
|
14555
14527
|
const createOrchServer = () => createOrchestratorMcpServer(client, customTools, workerManager);
|
|
14556
14528
|
const healthServer = startHealthServer(config.healthPort, log);
|
|
14557
14529
|
const sessionStore = new SessionStore();
|
|
14558
|
-
log.info("Starting gateway", { wsUrl: config.wsUrl
|
|
14530
|
+
log.info("Starting gateway", { wsUrl: config.wsUrl });
|
|
14559
14531
|
void startGateway({
|
|
14560
14532
|
client,
|
|
14561
14533
|
wsUrl: config.wsUrl,
|
|
@@ -14564,12 +14536,9 @@ async function startAgent(opts) {
|
|
|
14564
14536
|
log,
|
|
14565
14537
|
abortSignal: abortController.signal,
|
|
14566
14538
|
botId: localConfig.botId,
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
ticket: connectResult.ticket,
|
|
14571
|
-
wsUrl: connectResult.wsUrl
|
|
14572
|
-
},
|
|
14539
|
+
stage: localConfig.stage,
|
|
14540
|
+
initialWs: connectResult.ws,
|
|
14541
|
+
initialRotationToken: connectResult.rotationToken,
|
|
14573
14542
|
getWorkerStatus: () => ({
|
|
14574
14543
|
activeWorkers: workerManager.activeCount,
|
|
14575
14544
|
queuedTasks: workerManager.queuedCount,
|
|
@@ -14688,6 +14657,11 @@ async function startAgent(opts) {
|
|
|
14688
14657
|
log.info(
|
|
14689
14658
|
`Sleep cycle: consolidation applied (${consolidateResult.opsApplied} ops, ${consolidateResult.entriesArchived} archived)`
|
|
14690
14659
|
);
|
|
14660
|
+
if (consolidateResult.validationErrors?.length > 0) {
|
|
14661
|
+
log.warn(
|
|
14662
|
+
`Sleep cycle: validation errors: ${consolidateResult.validationErrors.join("; ")}`
|
|
14663
|
+
);
|
|
14664
|
+
}
|
|
14691
14665
|
} catch (parseErr) {
|
|
14692
14666
|
const parseMsg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
14693
14667
|
log.warn(`Sleep cycle: failed to submit consolidation: ${parseMsg}`);
|