@slock-ai/daemon 0.28.1-alpha.2 → 0.29.0

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.
@@ -81,6 +81,7 @@ server.tool(
81
81
  };
82
82
  } catch (err) {
83
83
  return {
84
+ isError: true,
84
85
  content: [{ type: "text", text: `Error: ${err.message}` }]
85
86
  };
86
87
  }
@@ -99,12 +100,14 @@ server.tool(
99
100
  const path = await import("path");
100
101
  if (!fs.existsSync(file_path)) {
101
102
  return {
103
+ isError: true,
102
104
  content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
103
105
  };
104
106
  }
105
107
  const stat = fs.statSync(file_path);
106
108
  if (stat.size > 5 * 1024 * 1024) {
107
109
  return {
110
+ isError: true,
108
111
  content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.` }]
109
112
  };
110
113
  }
@@ -119,6 +122,7 @@ server.tool(
119
122
  channelId = listData.channelId;
120
123
  } else {
121
124
  return {
125
+ isError: true,
122
126
  content: [{ type: "text", text: `Error: Could not resolve channel: ${channel}` }]
123
127
  };
124
128
  }
@@ -149,6 +153,7 @@ server.tool(
149
153
  const data = await res.json();
150
154
  if (!res.ok) {
151
155
  return {
156
+ isError: true,
152
157
  content: [{ type: "text", text: `Error: ${data.error}` }]
153
158
  };
154
159
  }
@@ -165,6 +170,7 @@ Use this ID in send_message's attachment_ids parameter to include it in a messag
165
170
  };
166
171
  } catch (err) {
167
172
  return {
173
+ isError: true,
168
174
  content: [{ type: "text", text: `Error: ${err.message}` }]
169
175
  };
170
176
  }
@@ -202,6 +208,7 @@ Use your Read tool to view this image.` }]
202
208
  });
203
209
  if (!res.ok) {
204
210
  return {
211
+ isError: true,
205
212
  content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
206
213
  };
207
214
  }
@@ -223,6 +230,7 @@ Use your Read tool to view this image.` }]
223
230
  };
224
231
  } catch (err) {
225
232
  return {
233
+ isError: true,
226
234
  content: [{ type: "text", text: `Error: ${err.message}` }]
227
235
  };
228
236
  }
@@ -240,7 +248,7 @@ server.tool(
240
248
  );
241
249
  if (!res.ok) {
242
250
  const errData = await res.json().catch(() => ({}));
243
- return { content: [{ type: "text", text: `Error: ${errData.error || res.statusText}` }] };
251
+ return { isError: true, content: [{ type: "text", text: `Error: ${errData.error || res.statusText}` }] };
244
252
  }
245
253
  const data = await res.json();
246
254
  if (data.messages?.length > 0) {
@@ -251,6 +259,7 @@ server.tool(
251
259
  };
252
260
  } catch (err) {
253
261
  return {
262
+ isError: true,
254
263
  content: [{ type: "text", text: `Error: ${err.message}` }]
255
264
  };
256
265
  }
@@ -263,7 +272,8 @@ function formatMessages(messages) {
263
272
  const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
264
273
  const senderType = m.sender_type === "agent" ? " type=agent" : "";
265
274
  const attachSuffix = m.attachments?.length ? ` [${m.attachments.length} image${m.attachments.length > 1 ? "s" : ""}: ${m.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
266
- return `[target=${target} msg=${msgId} time=${time}${senderType}] @${m.sender_name}: ${m.content}${attachSuffix}`;
275
+ const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${m.task_assignee_id ? ` assignee=${m.task_assignee_type}:${m.task_assignee_id}` : ""}]` : "";
276
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] @${m.sender_name}: ${m.content}${attachSuffix}${taskSuffix}`;
267
277
  }).join("\n");
268
278
  }
269
279
  server.tool(
@@ -315,6 +325,7 @@ server.tool(
315
325
  };
316
326
  } catch (err) {
317
327
  return {
328
+ isError: true,
318
329
  content: [{ type: "text", text: `Error: ${err.message}` }]
319
330
  };
320
331
  }
@@ -358,9 +369,10 @@ server.tool(
358
369
  const formatted = data.messages.map((m) => {
359
370
  const senderType = m.senderType === "agent" ? " type=agent" : "";
360
371
  const time = m.createdAt ? toLocalTime(m.createdAt) : "-";
361
- const msgId = m.id ? m.id.slice(0, 8) : "-";
372
+ const msgId = m.id || "-";
362
373
  const attachSuffix = m.attachments?.length ? ` [${m.attachments.length} image${m.attachments.length > 1 ? "s" : ""}: ${m.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
363
- return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] @${m.senderName}: ${m.content}${attachSuffix}`;
374
+ const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
375
+ return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] @${m.senderName}: ${m.content}${attachSuffix}${taskSuffix}`;
364
376
  }).join("\n");
365
377
  let footer = "";
366
378
  if (data.historyLimited) {
@@ -397,6 +409,7 @@ ${formatted}${footer}`
397
409
  };
398
410
  } catch (err) {
399
411
  return {
412
+ isError: true,
400
413
  content: [{ type: "text", text: `Error: ${err.message}` }]
401
414
  };
402
415
  }
@@ -404,7 +417,7 @@ ${formatted}${footer}`
404
417
  );
405
418
  server.tool(
406
419
  "list_tasks",
407
- "List tasks on a channel's task board. Returns tasks with their number (#t1, #t2...), title, status, and assignee.",
420
+ "List all tasks in a channel. Returns each task's number, title, status, assignee, and message ID. Use this to see what work exists before claiming. Tasks marked as legacy are from an older system and cannot be claimed or modified.",
408
421
  {
409
422
  channel: z.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
410
423
  status: z.enum(["all", "todo", "in_progress", "in_review", "done"]).default("all").describe("Filter by status (default: all)")
@@ -421,6 +434,7 @@ server.tool(
421
434
  const data = await res.json();
422
435
  if (!res.ok) {
423
436
  return {
437
+ isError: true,
424
438
  content: [{ type: "text", text: `Error: ${data.error}` }]
425
439
  };
426
440
  }
@@ -432,7 +446,9 @@ server.tool(
432
446
  const formatted = data.tasks.map((t) => {
433
447
  const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
434
448
  const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
435
- return `#t${t.taskNumber} [${t.status}] "${t.title}"${assignee}${creator}`;
449
+ const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
450
+ const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
451
+ return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
436
452
  }).join("\n");
437
453
  return {
438
454
  content: [
@@ -446,6 +462,7 @@ ${formatted}`
446
462
  };
447
463
  } catch (err) {
448
464
  return {
465
+ isError: true,
449
466
  content: [{ type: "text", text: `Error: ${err.message}` }]
450
467
  };
451
468
  }
@@ -453,7 +470,7 @@ ${formatted}`
453
470
  );
454
471
  server.tool(
455
472
  "create_tasks",
456
- "Create one or more tasks on a channel's task board. Returns the created task numbers.",
473
+ "Create one or more new tasks in a channel. Each task becomes a message in the chat flow with an assigned task number. Returns task numbers and message IDs. After creating, claim the task before starting work on it. Do not use this to convert an existing message \u2014 use claim_tasks with message_ids instead.",
457
474
  {
458
475
  channel: z.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
459
476
  tasks: z.array(
@@ -472,21 +489,27 @@ server.tool(
472
489
  const data = await res.json();
473
490
  if (!res.ok) {
474
491
  return {
492
+ isError: true,
475
493
  content: [{ type: "text", text: `Error: ${data.error}` }]
476
494
  };
477
495
  }
478
- const created = data.tasks.map((t) => `#t${t.taskNumber} "${t.title}"`).join("\n");
496
+ const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
497
+ const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 send_message to "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
479
498
  return {
480
499
  content: [
481
500
  {
482
501
  type: "text",
483
502
  text: `Created ${data.tasks.length} task(s) in ${channel}:
484
- ${created}`
503
+ ${created}
504
+
505
+ To follow up in each task's thread:
506
+ ${threadHints}`
485
507
  }
486
508
  ]
487
509
  };
488
510
  } catch (err) {
489
511
  return {
512
+ isError: true,
490
513
  content: [{ type: "text", text: `Error: ${err.message}` }]
491
514
  };
492
515
  }
@@ -494,48 +517,70 @@ ${created}`
494
517
  );
495
518
  server.tool(
496
519
  "claim_tasks",
497
- "Claim one or more tasks by their number. Returns which claims succeeded and which failed (e.g. already claimed by someone else).",
520
+ `Claim tasks so you are assigned to work on them. Two modes:
521
+ 1. By task number: claim existing tasks shown in list_tasks. Use task_numbers=[1, 3].
522
+ 2. By message ID: convert a regular message into a task and claim it. Use message_ids=["a1b2c3d4"]. The message ID is the 8-character msg= value from received messages or read_history.
523
+
524
+ If a task is in "todo" status, claiming auto-advances it to "in_progress". If another agent already claimed it, the claim fails \u2014 do not work on that task, move on. Always claim before starting any work to prevent duplicate effort.`,
498
525
  {
499
- channel: z.string().describe("The channel whose tasks to claim \u2014 e.g. '#engineering'"),
500
- task_numbers: z.array(z.number()).describe("Task numbers to claim (e.g. [1, 3, 5])")
526
+ channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
527
+ task_numbers: z.array(z.number()).optional().describe("Task numbers to claim (from list_tasks output, e.g. [1, 3])"),
528
+ message_ids: z.array(z.string()).optional().describe("Message IDs or short ID prefixes (the 8-char msg= value, e.g. ['a1b2c3d4']). Converts the message to a task and claims it.")
501
529
  },
502
- async ({ channel, task_numbers }) => {
530
+ async ({ channel, task_numbers, message_ids }) => {
503
531
  try {
532
+ if ((!task_numbers || task_numbers.length === 0) && (!message_ids || message_ids.length === 0)) {
533
+ return {
534
+ content: [{ type: "text", text: "Error: provide at least one of task_numbers or message_ids" }]
535
+ };
536
+ }
537
+ const body = { channel };
538
+ if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
539
+ if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
504
540
  const res = await fetch(
505
541
  `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
506
542
  {
507
543
  method: "POST",
508
544
  headers: commonHeaders,
509
- body: JSON.stringify({ channel, task_numbers })
545
+ body: JSON.stringify(body)
510
546
  }
511
547
  );
512
548
  const data = await res.json();
513
549
  if (!res.ok) {
514
550
  return {
551
+ isError: true,
515
552
  content: [{ type: "text", text: `Error: ${data.error}` }]
516
553
  };
517
554
  }
518
555
  const lines = data.results.map((r) => {
556
+ const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
519
557
  if (r.success) {
520
- return `#t${r.taskNumber}: claimed`;
558
+ const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
559
+ return `${label} (msg:${msgShort}): claimed`;
521
560
  }
522
- return `#t${r.taskNumber}: FAILED \u2014 ${r.reason || "already claimed"}`;
561
+ return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
523
562
  });
524
563
  const succeeded = data.results.filter((r) => r.success).length;
525
564
  const failed = data.results.length - succeeded;
526
565
  let summary = `${succeeded} claimed`;
527
566
  if (failed > 0) summary += `, ${failed} failed`;
567
+ const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} \u2192 send_message to "${channel}:${r.messageId.slice(0, 8)}"`).join("\n");
568
+ const threadHint = claimedMsgs ? `
569
+
570
+ Follow up in each task's thread:
571
+ ${claimedMsgs}` : "";
528
572
  return {
529
573
  content: [
530
574
  {
531
575
  type: "text",
532
576
  text: `Claim results (${summary}):
533
- ${lines.join("\n")}`
577
+ ${lines.join("\n")}${threadHint}`
534
578
  }
535
579
  ]
536
580
  };
537
581
  } catch (err) {
538
582
  return {
583
+ isError: true,
539
584
  content: [{ type: "text", text: `Error: ${err.message}` }]
540
585
  };
541
586
  }
@@ -543,7 +588,7 @@ ${lines.join("\n")}`
543
588
  );
544
589
  server.tool(
545
590
  "unclaim_task",
546
- "Release your claim on a task, setting it back to open.",
591
+ "Release your claim on a task so someone else can pick it up. Only use this if you can no longer work on the task \u2014 not as a way to mark it done. Use update_task_status to change status instead.",
547
592
  {
548
593
  channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
549
594
  task_number: z.number().describe("The task number to unclaim (e.g. 3)")
@@ -561,16 +606,18 @@ server.tool(
561
606
  const data = await res.json();
562
607
  if (!res.ok) {
563
608
  return {
609
+ isError: true,
564
610
  content: [{ type: "text", text: `Error: ${data.error}` }]
565
611
  };
566
612
  }
567
613
  return {
568
614
  content: [
569
- { type: "text", text: `#t${task_number} unclaimed \u2014 now open.` }
615
+ { type: "text", text: `#${task_number} unclaimed \u2014 now open.` }
570
616
  ]
571
617
  };
572
618
  } catch (err) {
573
619
  return {
620
+ isError: true,
574
621
  content: [{ type: "text", text: `Error: ${err.message}` }]
575
622
  };
576
623
  }
@@ -578,7 +625,7 @@ server.tool(
578
625
  );
579
626
  server.tool(
580
627
  "update_task_status",
581
- "Update a task's progress status. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review, in_progress\u2192done, in_review\u2192done, in_review\u2192in_progress. You must be the assignee (except in_review\u2192done which anyone can do).",
628
+ "Update a task's progress status. You must be the task's assignee to update it. Use in_review when your work is ready for human validation. Only set done for trivial tasks or after explicit approval. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review or done, in_review\u2192done or back to in_progress.",
582
629
  {
583
630
  channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
584
631
  task_number: z.number().describe("The task number to update (e.g. 3)"),
@@ -597,16 +644,18 @@ server.tool(
597
644
  const data = await res.json();
598
645
  if (!res.ok) {
599
646
  return {
647
+ isError: true,
600
648
  content: [{ type: "text", text: `Error: ${data.error}` }]
601
649
  };
602
650
  }
603
651
  return {
604
652
  content: [
605
- { type: "text", text: `#t${task_number} moved to ${status}.` }
653
+ { type: "text", text: `#${task_number} moved to ${status}.` }
606
654
  ]
607
655
  };
608
656
  } catch (err) {
609
657
  return {
658
+ isError: true,
610
659
  content: [{ type: "text", text: `Error: ${err.message}` }]
611
660
  };
612
661
  }
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ var DaemonConnection = class {
17
17
  reconnectDelay = 1e3;
18
18
  maxReconnectDelay = 3e4;
19
19
  shouldConnect = true;
20
+ reconnectAttempt = 0;
21
+ lastDroppedSendLogAt = 0;
20
22
  constructor(options) {
21
23
  this.options = options;
22
24
  }
@@ -31,6 +33,7 @@ var DaemonConnection = class {
31
33
  this.reconnectTimer = null;
32
34
  }
33
35
  if (this.ws) {
36
+ console.log("[Daemon] Disconnect requested");
34
37
  this.ws.close();
35
38
  this.ws = null;
36
39
  }
@@ -38,6 +41,12 @@ var DaemonConnection = class {
38
41
  send(msg) {
39
42
  if (this.ws?.readyState === WebSocket.OPEN) {
40
43
  this.ws.send(JSON.stringify(msg));
44
+ return;
45
+ }
46
+ const now = Date.now();
47
+ if (now - this.lastDroppedSendLogAt > 5e3) {
48
+ this.lastDroppedSendLogAt = now;
49
+ console.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
41
50
  }
42
51
  }
43
52
  get connected() {
@@ -50,6 +59,7 @@ var DaemonConnection = class {
50
59
  this.ws = new WebSocket(wsUrl);
51
60
  this.ws.on("open", () => {
52
61
  console.log("[Daemon] Connected to server");
62
+ this.reconnectAttempt = 0;
53
63
  this.reconnectDelay = 1e3;
54
64
  this.options.onConnect();
55
65
  });
@@ -61,8 +71,11 @@ var DaemonConnection = class {
61
71
  console.error("[Daemon] Invalid message from server:", err);
62
72
  }
63
73
  });
64
- this.ws.on("close", () => {
65
- console.log("[Daemon] Disconnected from server");
74
+ this.ws.on("close", (code, reasonBuffer) => {
75
+ const reason = reasonBuffer.toString("utf8");
76
+ console.log(
77
+ `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
78
+ );
66
79
  this.options.onDisconnect();
67
80
  this.scheduleReconnect();
68
81
  });
@@ -73,7 +86,8 @@ var DaemonConnection = class {
73
86
  scheduleReconnect() {
74
87
  if (!this.shouldConnect) return;
75
88
  if (this.reconnectTimer) return;
76
- console.log(`[Daemon] Reconnecting in ${this.reconnectDelay}ms...`);
89
+ this.reconnectAttempt += 1;
90
+ console.log(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
77
91
  this.reconnectTimer = setTimeout(() => {
78
92
  this.reconnectTimer = null;
79
93
  this.doConnect();
@@ -200,30 +214,29 @@ Each channel has a **name** and optionally a **description** that define its pur
200
214
 
201
215
  \`read_history(channel="#channel-name")\` or \`read_history(channel="dm:@peer-name")\` or \`read_history(channel="#channel:shortid")\`
202
216
 
203
- ### Task boards
217
+ ### Tasks
204
218
 
205
- Each channel has a task board with two independent dimensions: **status** (progress) and **assignee** (who's doing it).
219
+ When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
206
220
 
207
- **Status** (progress): \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
208
- - **todo**: Task exists, not started yet.
209
- - **in_progress**: Actively being worked on.
210
- - **in_review**: Work is done, awaiting human validation. Humans can see which tasks need their attention.
211
- - **done**: Accepted and finished. These are collapsed in the UI.
221
+ **Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
212
222
 
213
- **Assignee** is independent from status \u2014 you can claim/unclaim at any status (except done).
223
+ **What you see in messages:**
224
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
225
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
226
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
214
227
 
215
- **Tools:**
216
- - **View tasks**: \`list_tasks(channel="#channel-name")\` \u2014 see all tasks with status and assignee.
217
- - **Create tasks**: \`create_tasks(channel="#channel-name", tasks=[{title: "..."}, ...])\` \u2014 create one or more tasks.
218
- - **Claim tasks**: \`claim_tasks(channel="#channel-name", task_numbers=[1, 3])\` \u2014 assign yourself. If the task is \`todo\`, it auto-advances to \`in_progress\`. If another agent already claimed it, your claim fails.
219
- - **Unclaim**: \`unclaim_task(channel="#channel-name", task_number=3)\` \u2014 remove your assignment. Does not change progress status.
220
- - **Update status**: \`update_task_status(channel="#channel-name", task_number=3, status="in_review")\` \u2014 move a task to a new status. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review, in_progress\u2192done, in_review\u2192done, in_review\u2192in_progress.
228
+ \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
221
229
 
222
- **CRITICAL: You MUST claim a task before starting ANY work on it.** Call \`${t("claim_tasks")}\` first. If the claim fails (someone else already claimed it), you MUST NOT work on that task \u2014 move on to another one. This is the only way to prevent duplicate work across agents. No exceptions.
230
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
223
231
 
224
- **IMPORTANT: When you finish a task, use \`update_task_status(..., status="in_review")\`.** This gives humans a chance to validate your work before it's marked as done. Only set status to \`done\` directly for trivial tasks that don't need review.
232
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
225
233
 
226
- **IMPORTANT: After someone approves your work** (e.g. says "merge it", "looks good", "approved", "review passed"), **you must set the task to \`done\` yourself** if the reviewer doesn't do it. Don't leave tasks in \`in_review\` after they've been approved.
234
+ **Workflow:**
235
+ 1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
236
+ 2. If the claim fails, someone else is working on it \u2014 do not start, move on
237
+ 3. Post updates in the task's thread: \`send_message(target="#channel:msgShortId", ...)\`
238
+ 4. When done, set status to \`in_review\` so a human can validate
239
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
227
240
 
228
241
  ### Splitting tasks for parallel execution
229
242
 
@@ -261,7 +274,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
261
274
 
262
275
  ### Formatting \u2014 No HTML
263
276
 
264
- Never output raw HTML tags in your messages. Use plain-text @mentions (e.g. \`@alice\`) and #channel references (e.g. \`#general\`, \`#t1\`). Do NOT wrap them in \`<a>\` tags or any other HTML.
277
+ Never output raw HTML tags in your messages. Use plain-text @mentions (e.g. \`@alice\`) and #channel references (e.g. \`#general\`, \`#1\`). Do NOT wrap them in \`<a>\` tags or any other HTML.
265
278
 
266
279
  When you intend to reference a channel or mention someone, write them as plain text \u2014 do NOT wrap them in backticks (inline code). Backtick-wrapped mentions render as code instead of interactive links.
267
280
 
@@ -563,13 +576,13 @@ var ClaudeDriver = class {
563
576
  if (name === "mcp__chat__create_tasks") return input.channel || "";
564
577
  if (name === "mcp__chat__claim_tasks") {
565
578
  const nums = input.task_numbers;
566
- return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
579
+ return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
567
580
  }
568
581
  if (name === "mcp__chat__unclaim_task") {
569
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
582
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
570
583
  }
571
584
  if (name === "mcp__chat__update_task_status") {
572
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
585
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
573
586
  }
574
587
  if (name === "mcp__chat__upload_file") return input.file_path || "";
575
588
  return "";
@@ -807,13 +820,13 @@ var CodexDriver = class {
807
820
  if (name === `${this.mcpToolPrefix}create_tasks`) return input.channel || "";
808
821
  if (name === `${this.mcpToolPrefix}claim_tasks`) {
809
822
  const nums = input.task_numbers;
810
- return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
823
+ return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#") : nums}` : "";
811
824
  }
812
825
  if (name === `${this.mcpToolPrefix}unclaim_task`) {
813
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
826
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
814
827
  }
815
828
  if (name === `${this.mcpToolPrefix}update_task_status`) {
816
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
829
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
817
830
  }
818
831
  if (name === `${this.mcpToolPrefix}upload_file`) return input.file_path || "";
819
832
  return "";
@@ -858,18 +871,26 @@ function buildUnreadSummary(messages, excludeChannel) {
858
871
  }
859
872
  var MAX_TRAJECTORY_TEXT = 2e3;
860
873
  var ACTIVITY_HEARTBEAT_MS = 6e4;
874
+ var MAX_STDOUT_LINES = 8;
875
+ var MAX_STDOUT_LINE_LENGTH = 240;
861
876
  var MAX_STDERR_LINES = 8;
862
877
  var MAX_STDERR_LINE_LENGTH = 240;
863
- function pushRecentStderr(lines, chunk) {
878
+ function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
864
879
  const next = [...lines];
865
880
  for (const rawLine of chunk.split(/\r?\n/)) {
866
881
  const text = rawLine.trim();
867
882
  if (!text) continue;
868
883
  next.push(
869
- text.length > MAX_STDERR_LINE_LENGTH ? `${text.slice(0, MAX_STDERR_LINE_LENGTH)}...` : text
884
+ text.length > maxLineLength ? `${text.slice(0, maxLineLength)}...` : text
870
885
  );
871
886
  }
872
- return next.slice(-MAX_STDERR_LINES);
887
+ return next.slice(-maxLines);
888
+ }
889
+ function pushRecentStderr(lines, chunk) {
890
+ return pushRecentLines(lines, chunk, MAX_STDERR_LINES, MAX_STDERR_LINE_LENGTH);
891
+ }
892
+ function pushRecentStdout(lines, chunk) {
893
+ return pushRecentLines(lines, chunk, MAX_STDOUT_LINES, MAX_STDOUT_LINE_LENGTH);
873
894
  }
874
895
  function formatCrashReason(code, signal, ap) {
875
896
  const parts = [];
@@ -889,6 +910,9 @@ function formatCrashReason(code, signal, ap) {
889
910
  if (ap.recentStderr.length > 0) {
890
911
  parts.push(`stderr: ${ap.recentStderr.join(" | ")}`);
891
912
  }
913
+ if (!ap.lastRuntimeError && ap.recentStdout.length > 0) {
914
+ parts.push(`stdout: ${ap.recentStdout.join(" | ")}`);
915
+ }
892
916
  return parts.join(" | ");
893
917
  }
894
918
  function summarizeCrash(code, signal) {
@@ -896,6 +920,11 @@ function summarizeCrash(code, signal) {
896
920
  if (typeof code === "number") return `exit code ${code}`;
897
921
  return "unknown exit";
898
922
  }
923
+ function isMissingResumeSession(ap) {
924
+ if (ap.driver.id !== "claude") return false;
925
+ if (!ap.sessionId) return false;
926
+ return /No conversation found with session ID/i.test(ap.lastRuntimeError || "");
927
+ }
899
928
  function getMessageDeliveryText(supportsStdinNotification) {
900
929
  return supportsStdinNotification ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
901
930
  }
@@ -919,7 +948,14 @@ var AgentProcessManager = class _AgentProcessManager {
919
948
  this.driverResolver = opts.driverResolver || getDriver;
920
949
  }
921
950
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt) {
922
- if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
951
+ if (this.agents.has(agentId)) {
952
+ console.log(`[Agent ${agentId}] Start ignored (already running)`);
953
+ return;
954
+ }
955
+ if (this.agentsStarting.has(agentId)) {
956
+ console.log(`[Agent ${agentId}] Start ignored (startup in progress)`);
957
+ return;
958
+ }
923
959
  this.agentsStarting.add(agentId);
924
960
  try {
925
961
  const driver = this.driverResolver(config.runtime || "claude");
@@ -1025,6 +1061,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1025
1061
  activityHeartbeat: null,
1026
1062
  lastActivity: "",
1027
1063
  lastActivityDetail: "",
1064
+ recentStdout: [],
1028
1065
  recentStderr: [],
1029
1066
  lastRuntimeError: null,
1030
1067
  spawnError: null,
@@ -1036,7 +1073,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1036
1073
  this.agentsStarting.delete(agentId);
1037
1074
  let buffer = "";
1038
1075
  proc.stdout?.on("data", (chunk) => {
1039
- buffer += chunk.toString();
1076
+ const chunkText = chunk.toString();
1077
+ const current = this.agents.get(agentId);
1078
+ if (current) {
1079
+ current.recentStdout = pushRecentStdout(current.recentStdout, chunkText);
1080
+ }
1081
+ buffer += chunkText;
1040
1082
  const lines = buffer.split("\n");
1041
1083
  buffer = lines.pop() || "";
1042
1084
  for (const line of lines) {
@@ -1087,6 +1129,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1087
1129
  const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
1088
1130
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
1089
1131
  if (queuedWakeMessage) {
1132
+ console.log(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
1090
1133
  const nextConfig = { ...ap.config, sessionId: ap.sessionId };
1091
1134
  this.idleAgentConfigs.set(agentId, {
1092
1135
  config: nextConfig,
@@ -1108,11 +1151,33 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1108
1151
  config: { ...ap.config, sessionId: ap.sessionId },
1109
1152
  sessionId: ap.sessionId
1110
1153
  });
1154
+ if (!ap.driver.supportsStdinNotification) {
1155
+ console.log(`[Agent ${agentId}] Turn completed; cached idle state for future restart`);
1156
+ }
1111
1157
  this.broadcastActivity(agentId, "online", "Process idle");
1112
1158
  } else {
1113
1159
  this.idleAgentConfigs.delete(agentId);
1114
1160
  const reason = formatCrashReason(finalCode, finalSignal, ap);
1115
1161
  const summary = summarizeCrash(finalCode, finalSignal);
1162
+ if (isMissingResumeSession(ap)) {
1163
+ const staleSessionId = ap.sessionId;
1164
+ const restartConfig = { ...ap.config, sessionId: null };
1165
+ console.warn(
1166
+ `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
1167
+ );
1168
+ this.broadcastActivity(
1169
+ agentId,
1170
+ "working",
1171
+ "Stored Claude session missing; cold-starting a new session\u2026",
1172
+ [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
1173
+ );
1174
+ this.startAgent(agentId, restartConfig).catch((err) => {
1175
+ console.error(`[Agent ${agentId}] Cold start recovery failed:`, err);
1176
+ this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1177
+ this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
1178
+ });
1179
+ return;
1180
+ }
1116
1181
  console.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
1117
1182
  this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1118
1183
  this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
@@ -1129,7 +1194,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1129
1194
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
1130
1195
  this.idleAgentConfigs.delete(agentId);
1131
1196
  const ap = this.agents.get(agentId);
1132
- if (!ap) return;
1197
+ if (!ap) {
1198
+ if (!silent) {
1199
+ console.log(`[Agent ${agentId}] Stop requested but no running process was found`);
1200
+ }
1201
+ return;
1202
+ }
1133
1203
  if (ap.notificationTimer) {
1134
1204
  clearTimeout(ap.notificationTimer);
1135
1205
  }
@@ -1141,10 +1211,14 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1141
1211
  if (!silent) {
1142
1212
  this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1143
1213
  this.broadcastActivity(agentId, "offline", "Stopped");
1214
+ console.log(`[Agent ${agentId}] Stopped by request`);
1144
1215
  }
1145
1216
  if (wait) {
1146
1217
  await new Promise((resolve) => {
1147
1218
  const forceKillTimer = setTimeout(() => {
1219
+ if (!silent) {
1220
+ console.warn(`[Agent ${agentId}] Stop timed out; force killing`);
1221
+ }
1148
1222
  try {
1149
1223
  ap.process.kill("SIGKILL");
1150
1224
  } catch {
@@ -1173,7 +1247,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1173
1247
  }
1174
1248
  const cached = this.idleAgentConfigs.get(agentId);
1175
1249
  if (cached) {
1176
- console.log(`[Agent ${agentId}] Auto-restarting idle process for incoming message`);
1250
+ console.log(`[Agent ${agentId}] Starting from idle state for new message`);
1177
1251
  this.idleAgentConfigs.delete(agentId);
1178
1252
  this.startAgent(agentId, cached.config, message).catch((err) => {
1179
1253
  console.error(`[Agent ${agentId}] Failed to auto-restart:`, err);
@@ -1201,9 +1275,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1201
1275
  const agentDataDir = path3.join(this.dataDir, agentId);
1202
1276
  try {
1203
1277
  await rm(agentDataDir, { recursive: true, force: true });
1204
- console.log(`[Agent ${agentId}] Workspace deleted: ${agentDataDir}`);
1278
+ console.log(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
1205
1279
  } catch (err) {
1206
- console.error(`[Agent ${agentId}] Failed to delete workspace:`, err);
1280
+ console.error(`[Agent ${agentId}] Workspace reset failed:`, err);
1207
1281
  }
1208
1282
  }
1209
1283
  async stopAll() {
@@ -1628,6 +1702,31 @@ var RUNTIMES = [
1628
1702
  // src/index.ts
1629
1703
  var require2 = createRequire(import.meta.url);
1630
1704
  var DAEMON_VERSION = require2("../package.json").version;
1705
+ function formatChannelTarget(msg) {
1706
+ return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
1707
+ }
1708
+ function summarizeIncomingMessage(msg) {
1709
+ switch (msg.type) {
1710
+ case "agent:start":
1711
+ return `(agent=${msg.agentId}, runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`;
1712
+ case "agent:stop":
1713
+ return `(agent=${msg.agentId})`;
1714
+ case "agent:reset-workspace":
1715
+ return `(agent=${msg.agentId})`;
1716
+ case "agent:deliver":
1717
+ return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
1718
+ case "agent:workspace:list":
1719
+ return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
1720
+ case "agent:workspace:read":
1721
+ return `(agent=${msg.agentId}, path=${msg.path})`;
1722
+ case "agent:skills:list":
1723
+ return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
1724
+ case "machine:workspace:delete":
1725
+ return `(directory=${msg.directoryName})`;
1726
+ default:
1727
+ return "";
1728
+ }
1729
+ }
1631
1730
  function detectRuntimes() {
1632
1731
  const detected = [];
1633
1732
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -1666,27 +1765,28 @@ connection = new DaemonConnection({
1666
1765
  serverUrl,
1667
1766
  apiKey,
1668
1767
  onMessage: (msg) => {
1669
- console.log(`[Daemon] Received: ${msg.type}`, msg.type === "ping" ? "" : JSON.stringify(msg).slice(0, 200));
1768
+ const summary = summarizeIncomingMessage(msg);
1769
+ console.log(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
1670
1770
  switch (msg.type) {
1671
1771
  case "agent:start":
1672
- console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
1772
+ console.log(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
1673
1773
  agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt).catch((err) => {
1674
1774
  const reason = err instanceof Error ? err.message : String(err);
1675
- console.error(`[Daemon] Failed to start agent ${msg.agentId}:`, reason);
1775
+ console.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
1676
1776
  connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive" });
1677
1777
  connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}` });
1678
1778
  });
1679
1779
  break;
1680
1780
  case "agent:stop":
1681
- console.log(`[Daemon] Stopping agent ${msg.agentId}`);
1781
+ console.log(`[Agent ${msg.agentId}] Stop requested`);
1682
1782
  agentManager.stopAgent(msg.agentId);
1683
1783
  break;
1684
1784
  case "agent:reset-workspace":
1685
- console.log(`[Daemon] Resetting workspace for agent ${msg.agentId}`);
1785
+ console.log(`[Agent ${msg.agentId}] Workspace reset requested`);
1686
1786
  agentManager.resetWorkspace(msg.agentId);
1687
1787
  break;
1688
1788
  case "agent:deliver":
1689
- console.log(`[Daemon] Delivering message to ${msg.agentId}: ${msg.message.content.slice(0, 80)}`);
1789
+ console.log(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
1690
1790
  agentManager.deliverMessage(msg.agentId, msg.message);
1691
1791
  connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
1692
1792
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.28.1-alpha.2",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"