@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 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, existsSync, unlinkSync, appendFileSync, readFileSync, statSync, createWriteStream } from 'fs';
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(token, name2) {
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
- writeFileSync(
129
- paths.envFile,
130
- `DESKFREE_LAUNCH=${token}
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(token, name2) {
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
- writeFileSync(
224
- paths.envFile,
225
- `DESKFREE_LAUNCH=${token}
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(token, name2) {
259
+ function install(botId, name2, stage) {
268
260
  if (process.platform === "darwin") {
269
- installMac(token, name2);
261
+ installMac(botId, name2, stage);
270
262
  } else if (process.platform === "linux") {
271
- installLinux(token, name2);
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.PROPOSE, async (params) => {
2732
+ createTool(ORCHESTRATOR_TOOLS.CREATE_TASK, async (params) => {
2744
2733
  try {
2745
- const context = validateStringParam(params, "context", false);
2746
- const taskId = validateStringParam(params, "taskId", false);
2747
- const rawTasks = params.tasks;
2748
- if (!Array.isArray(rawTasks) || rawTasks.length === 0) {
2749
- throw new Error("tasks must be a non-empty array of task objects");
2750
- }
2751
- const tasks = rawTasks;
2752
- for (let i = 0; i < tasks.length; i++) {
2753
- const task = tasks[i];
2754
- if (!task || typeof task !== "object") {
2755
- throw new Error(`tasks[${i}] must be an object`);
2756
- }
2757
- if (!task.title || typeof task.title !== "string" || task.title.trim() === "") {
2758
- throw new Error(`tasks[${i}].title must be a non-empty string`);
2759
- }
2760
- }
2761
- await client.proposePlan({
2762
- context,
2763
- tasks,
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: `Proposal created with ${tasks.length} task(s)`
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.REOPEN_TASK, async (params) => {
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.reopenTask({ taskId, reason });
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}" reopened`,
2798
- reason ? [`Reason: ${reason}`] : []
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.PROPOSE, async (params) => {
2976
+ createTool(WORKER_TOOLS.CREATE_TASK, async (params) => {
2979
2977
  try {
2980
- const context = validateStringParam(params, "context", false);
2981
- const taskId = validateStringParam(params, "taskId", false);
2982
- const rawTasks = params.tasks;
2983
- if (!Array.isArray(rawTasks) || rawTasks.length === 0) {
2984
- throw new Error("tasks must be a non-empty array of task objects");
2985
- }
2986
- const tasks = rawTasks;
2987
- for (let i = 0; i < tasks.length; i++) {
2988
- const task = tasks[i];
2989
- if (!task || typeof task !== "object") {
2990
- throw new Error(`tasks[${i}] must be an object`);
2991
- }
2992
- if (!task.title || typeof task.title !== "string" || task.title.trim() === "") {
2993
- throw new Error(`tasks[${i}].title must be a non-empty string`);
2994
- }
2995
- }
2996
- await client.proposePlan({
2997
- context,
2998
- tasks,
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: `Proposal created with ${tasks.length} task(s)`
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.COMPLETE_TASK, async (params) => {
3025
+ createTool(WORKER_TOOLS.UPDATE_TASK_STATUS, async (params) => {
3029
3026
  try {
3030
3027
  const taskId = validateStringParam(params, "taskId", true);
3031
- const humanApproved = Boolean(params.humanApproved);
3032
- const result = await client.completeTask({
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
- humanApproved
3043
+ status: status2,
3044
+ awaiting,
3045
+ reason
3035
3046
  });
3036
- try {
3037
- options?.onTaskCompleted?.(taskId);
3038
- } catch {
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}" completed successfully`,
3043
- [humanApproved ? "Status: Human approved" : "Status: Completed"]
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 propose a task and move the work to a thread.
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. **Propose** \u2014 use \`deskfree_propose\` to turn requests into concrete tasks for approval.
3117
- 3. **Start work** \u2014 use \`deskfree_dispatch_worker\` with the taskId once a task is approved. You'll then continue the work in the task thread.
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 proposing, qualify the request.** Figure out what kind of thing this is:
3121
- - **One-off task** ("proofread this") \u2014 propose a task directly.
3122
- - **New aspiration** ("I want to start posting on LinkedIn") \u2014 don't rush to propose. Ask 1-2 short qualifying questions to understand the real goal.
3123
- - Never call \`deskfree_propose\` as your very first action \u2014 qualify first, even if briefly.
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 propose and coordinate \u2014 the actual work happens in task threads. Use \`deskfree_dispatch_worker\` to start working on approved tasks.
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 propose it as a new task (don't reopen the old one).
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, deskfree_complete_task, deskfree_send_message, deskfree_propose.
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 Send a brief \`notify\` message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
3169
- - **Judgment calls or creative direction?** State your assumptions and approach, send as \`ask\`, and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
3170
- - **Straightforward execution?** Proceed immediately after the notify \u2014 don't wait for a response.
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 Send an \`ask\` message when work is ready for review. Only complete (\`deskfree_complete_task\` with humanApproved: true) after the human has confirmed. Never self-complete.
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 an \`ask\` message and wait. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
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 proposals entirely \u2014 the human has enough on their plate. Focus only on dispatching approved work.
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. Propose as a task via \`deskfree_propose\`. One focused task, not a batch.
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, deskfree_propose, deskfree_send_message, deskfree_learning.
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 propose via \`deskfree_propose\` if needed.
3339
- 4. One proactive proposal max. Skip if nothing merits it or board is busy (3+ items needing human attention).`;
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, propose overnight work, and brief the human.
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, deskfree_propose, deskfree_send_message, deskfree_read_file, deskfree_orient.
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. PROPOSE THE PLAN
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 proposal max \u2014 or skip entirely. Don't pile on.
3382
- 2. Use \`deskfree_propose\` with 1-3 well-scoped tasks. Quality over quantity.
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 the proposal entirely. Don't force it.
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 proposals, still send a brief day summary.
3414
+ - If no tasks created, still send a brief day summary.
3393
3415
 
3394
3416
  ### Rules
3395
- - Do NOT propose things already on the board or recently completed.
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, propose it.
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(hostname) {
3544
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".local") || hostname.endsWith(".localhost") || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
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 validateBotToken(value) {
3547
- const fieldError = validateField({ value, name: "Bot token" });
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 (!trimmed.startsWith("bot_")) {
3551
- return 'Bot token must start with "bot_" (check your DeskFree bot configuration)';
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 token",
3577
+ name: "Bot ID",
3556
3578
  minLength: 10,
3557
- maxLength: 200,
3558
- pattern: /^bot_[a-zA-Z0-9_-]+$/,
3559
- patternMessage: 'Bot token contains invalid characters. Only alphanumeric, underscore, and dash are allowed after "bot_"'
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
- botToken;
7370
+ getToken;
7355
7371
  apiUrl;
7356
7372
  requestTimeoutMs;
7357
- constructor(botToken, apiUrl, options) {
7358
- this.botToken = botToken;
7359
- this.apiUrl = apiUrl.replace(/\/$/, "");
7360
- this.requestTimeoutMs = options?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
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: `Bot ${this.botToken}`,
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
- /** Complete a task. Marks it as done. */
7502
- async completeTask(input) {
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.reopen", input);
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
- /** Propose a plancreates a proposal message with plan metadata. No DB rows until human approves. */
7549
- async proposePlan(input) {
7550
- if (!input.tasks || input.tasks.length === 0) {
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: `Bot ${this.botToken}`,
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
- REOPEN_TASK: {
7926
- name: "deskfree_reopen_task",
7927
- description: "Reopen a task (from review or done) back to pending. Use when more work is needed on a task. Works on both completed and in-review tasks.",
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 reopen" }),
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: "Why this task is being reopened (shown in task thread as system message)"
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
- PROPOSE: {
7955
- name: "deskfree_propose",
7956
- description: "Propose a plan for human approval. Nothing is created until the human reviews and approves in a modal. 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.",
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
- context: Type.Optional(
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: "Why this plan is being proposed \u2014 helps the human understand the reasoning behind the proposal"
7973
+ description: "Detailed instructions, constraints, or context for the task."
7961
7974
  })
7962
7975
  ),
7963
- tasks: Type.Array(
7964
- Type.Object({
7965
- title: Type.String({
7966
- description: "Task title \u2014 short, action-oriented (max 200 chars)"
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
- taskId: Type.Optional(
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: "Task ID to thread this proposal into (for follow-up proposals from within a task)"
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
- PROPOSE: {
8103
- name: "deskfree_propose",
8104
- description: "Propose a plan for human approval. Nothing is created until the human reviews and approves in a modal. 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.",
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
- context: Type.Optional(
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: "Why this plan is being proposed \u2014 helps the human understand the reasoning behind the proposal"
8094
+ description: "Detailed instructions, constraints, or context for the task."
8109
8095
  })
8110
8096
  ),
8111
- tasks: Type.Array(
8112
- Type.Object({
8113
- title: Type.String({
8114
- description: "Task title \u2014 short, action-oriented (max 200 chars)"
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
- taskId: Type.Optional(
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: "Task ID to thread this proposal into (for follow-up proposals from within a task)"
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
- COMPLETE_TASK: SHARED_TOOLS.COMPLETE_TASK,
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
- PROPOSE: SHARED_TOOLS.PROPOSE
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 approved tasks, then start working on them.
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 propose a task and move the work to a thread.
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: propose \u2192 approve \u2192 work.**
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. **Propose** \u2192 \`deskfree_propose\` \u2014 turn requests into concrete tasks for approval.
8246
- 3. **Start work** \u2192 \`deskfree_dispatch_worker\` with the taskId. You'll then continue the work in the task thread.
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 proposing, qualify the request.** Figure out what kind of thing this is:
8250
- - **One-off task** ("proofread this") \u2192 propose a task directly.
8251
- - **New aspiration** ("I want to start posting on LinkedIn") \u2192 don't rush to propose. Ask 1-2 short qualifying questions to understand the real goal.
8252
- - Never call \`deskfree_propose\` as your very first action \u2014 qualify first, even if briefly.
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 propose and coordinate \u2014 the actual work happens in task threads. Use \`deskfree_dispatch_worker\` to start working on approved tasks.
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 propose it as a new task (don't reopen the old one).
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, deskfree_complete_task, deskfree_send_message, deskfree_propose.
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 Send a brief \`notify\` message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
8294
- - **Judgment calls or creative direction?** State your assumptions and approach, send as \`ask\`, and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
8295
- - **Straightforward execution?** Proceed immediately after the notify \u2014 don't wait for a response.
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 Send an \`ask\` message when work is ready for review. Only complete (\`deskfree_complete_task\` with humanApproved: true) after the human has confirmed. Never self-complete.
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 an \`ask\` message and wait. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
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 requireEnv(key) {
8648
- const val = process.env[key];
8649
- if (!val) {
8650
- throw new Error(`Required environment variable ${key} is not set`);
8651
- }
8652
- return val;
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
- let botToken;
8656
- let apiUrl;
8657
- const launch = process.env["DESKFREE_LAUNCH"];
8658
- if (launch) {
8659
- try {
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 tokenError = validateBotToken(botToken);
8681
- if (tokenError !== null) {
8682
- throw new Error(`Invalid bot token: ${tokenError}`);
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
- botToken,
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 import_websocket2, wrapper_default2;
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
- const { ticket, wsUrl } = await client.getWsTicket();
12629
- resetBackoff(backoff);
12630
- if (totalReconnects > 0) {
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
- `Got WS ticket, reconnecting to ${wsUrl}... (reconnect #${totalReconnects})`
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
- ticket,
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
- if (message.includes("API error") || message.includes("authentication failed") || message.includes("server error")) {
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
- `Ticket fetch failed (attempt #${totalReconnects}): ${message}. Falling back to polling.`
12760
+ `Connection setup failed (attempt #${totalReconnects}): ${message}. Will retry after backoff.`
12659
12761
  );
12660
- reportError("error", `WS ticket fetch failed: ${message}`, {
12762
+ reportError("error", `Connection setup failed: ${message}`, {
12661
12763
  component: "gateway",
12662
- event: "ticket_fetch_failed",
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 { ticket, wsUrl, client, accountId, stateDir, log, abortSignal } = opts;
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 isConnected = false;
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
- connectionTimer = setTimeout(() => {
12725
- if (!isConnected) {
12726
- cleanup();
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
- ws.close();
12729
- } catch {
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
- }, PING_INTERVAL_MS);
12765
- if (opts.getWorkerStatus) {
12766
- try {
12767
- const status2 = opts.getWorkerStatus();
12768
- ws.send(
12769
- JSON.stringify({
12770
- action: "heartbeatResponse",
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 errMsg = err instanceof Error ? err.message : String(err);
12776
- log.warn(`Failed to send initial heartbeat: ${errMsg}`);
12834
+ const msg = err instanceof Error ? err.message : String(err);
12835
+ log.warn(`Failed to send ping: ${msg}`);
12777
12836
  }
12778
12837
  }
12779
- void pollAndDeliver(
12780
- client,
12781
- accountId,
12782
- stateDir,
12783
- cursor,
12784
- opts.onMessage,
12785
- log
12786
- ).then((result) => {
12787
- if (result.cursor) {
12788
- cursor = result.cursor;
12789
- saveCursor(ctx, cursor, stateDir, log);
12790
- }
12791
- }).catch((err) => {
12792
- const msg = err instanceof Error ? err.message : String(err);
12793
- log.warn(`Initial poll failed: ${msg}`);
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
- log.debug("Received pong \u2014 connection healthy");
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
- async function runPollingFallback(opts) {
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
- MAX_POLLING_ITERATIONS = 10;
13060
- MAX_CONSECUTIVE_POLL_FAILURES = 5;
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 hostname = parsed.hostname.toLowerCase();
13278
- const bareHostname = hostname.replace(/^\[|\]$/g, "");
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(hostname)) {
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.reopenTask({
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 client = new DeskFreeClient(localConfig.botToken, localConfig.apiUrl);
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, apiUrl: config.apiUrl });
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 result = runOneShotWorker({
15142
+ const agentStream = runOneShotWorker({
14930
15143
  prompt,
14931
15144
  systemPrompt: buildSleepDirective(agentContext),
14932
15145
  workerServer,
14933
15146
  model: config.model
14934
15147
  });
14935
- for await (const _ of result) {
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
- let token = cleanArgs[1];
15035
- if (!token) {
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
- token = await new Promise((resolve) => {
15305
+ botId = await new Promise((resolve) => {
15042
15306
  rl.question(
15043
- "Paste your bot token (from the DeskFree dashboard):\n> ",
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 (!token) {
15051
- console.error("No token provided.");
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(token, name);
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
- let token;
15083
- if (command === "start") {
15084
- token = cleanArgs[1];
15085
- } else if (command && !command.startsWith("-")) {
15086
- token = command;
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));