@questionbase/deskfree 0.6.2 → 0.6.3

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