@slock-ai/daemon 0.28.1-alpha.3 → 0.29.1-alpha.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
@@ -6,31 +6,48 @@ import os2 from "os";
6
6
  import { createRequire } from "module";
7
7
  import { execSync as execSync2 } from "child_process";
8
8
  import { accessSync } from "fs";
9
+ import { readFile as readFile2, readdir as readdir2, stat as stat2, mkdir as mkdir2, appendFile } from "fs/promises";
9
10
  import { fileURLToPath } from "url";
10
11
 
11
12
  // src/connection.ts
12
13
  import WebSocket from "ws";
14
+ var systemClock = {
15
+ now: () => Date.now(),
16
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
17
+ clearTimeout: (timer) => clearTimeout(timer)
18
+ };
19
+ var INBOUND_WATCHDOG_MS = 7e4;
13
20
  var DaemonConnection = class {
14
21
  ws = null;
15
22
  options;
23
+ clock;
16
24
  reconnectTimer = null;
17
- reconnectDelay = 1e3;
25
+ watchdogTimer = null;
26
+ reconnectDelay;
18
27
  maxReconnectDelay = 3e4;
19
28
  shouldConnect = true;
29
+ reconnectAttempt = 0;
30
+ lastDroppedSendLogAt = 0;
20
31
  constructor(options) {
21
32
  this.options = options;
33
+ this.clock = options.clock ?? systemClock;
34
+ this.reconnectDelay = options.minReconnectDelayMs ?? 1e3;
22
35
  }
23
36
  connect() {
24
37
  this.shouldConnect = true;
38
+ if (this.reconnectTimer) return;
39
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
25
40
  this.doConnect();
26
41
  }
27
42
  disconnect() {
28
43
  this.shouldConnect = false;
44
+ this.clearWatchdog();
29
45
  if (this.reconnectTimer) {
30
- clearTimeout(this.reconnectTimer);
46
+ this.clock.clearTimeout(this.reconnectTimer);
31
47
  this.reconnectTimer = null;
32
48
  }
33
49
  if (this.ws) {
50
+ console.log("[Daemon] Disconnect requested");
34
51
  this.ws.close();
35
52
  this.ws = null;
36
53
  }
@@ -38,6 +55,12 @@ var DaemonConnection = class {
38
55
  send(msg) {
39
56
  if (this.ws?.readyState === WebSocket.OPEN) {
40
57
  this.ws.send(JSON.stringify(msg));
58
+ return;
59
+ }
60
+ const now = this.clock.now();
61
+ if (now - this.lastDroppedSendLogAt > 5e3) {
62
+ this.lastDroppedSendLogAt = now;
63
+ console.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
41
64
  }
42
65
  }
43
66
  get connected() {
@@ -45,15 +68,23 @@ var DaemonConnection = class {
45
68
  }
46
69
  doConnect() {
47
70
  if (!this.shouldConnect) return;
71
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
48
72
  const wsUrl = this.options.serverUrl.replace(/^http/, "ws") + `/daemon/connect?key=${this.options.apiKey}`;
49
73
  console.log(`[Daemon] Connecting to ${this.options.serverUrl}...`);
50
- this.ws = new WebSocket(wsUrl);
51
- this.ws.on("open", () => {
74
+ const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl) : new WebSocket(wsUrl);
75
+ this.ws = ws;
76
+ ws.on("open", () => {
77
+ if (this.ws !== ws) return;
78
+ if (!this.shouldConnect) return;
52
79
  console.log("[Daemon] Connected to server");
53
- this.reconnectDelay = 1e3;
80
+ this.reconnectAttempt = 0;
81
+ this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
82
+ this.resetWatchdog();
54
83
  this.options.onConnect();
55
84
  });
56
- this.ws.on("message", (data) => {
85
+ ws.on("message", (data) => {
86
+ if (this.ws !== ws) return;
87
+ this.resetWatchdog();
57
88
  try {
58
89
  const msg = JSON.parse(data.toString());
59
90
  this.options.onMessage(msg);
@@ -61,25 +92,50 @@ var DaemonConnection = class {
61
92
  console.error("[Daemon] Invalid message from server:", err);
62
93
  }
63
94
  });
64
- this.ws.on("close", () => {
65
- console.log("[Daemon] Disconnected from server");
95
+ ws.on("close", (code, reasonBuffer) => {
96
+ if (this.ws !== ws) return;
97
+ this.ws = null;
98
+ this.clearWatchdog();
99
+ const reason = reasonBuffer.toString("utf8");
100
+ console.log(
101
+ `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
102
+ );
66
103
  this.options.onDisconnect();
67
104
  this.scheduleReconnect();
68
105
  });
69
- this.ws.on("error", (err) => {
106
+ ws.on("error", (err) => {
107
+ if (this.ws !== ws) return;
70
108
  console.error("[Daemon] WebSocket error:", err.message);
71
109
  });
72
110
  }
73
111
  scheduleReconnect() {
74
112
  if (!this.shouldConnect) return;
75
113
  if (this.reconnectTimer) return;
76
- console.log(`[Daemon] Reconnecting in ${this.reconnectDelay}ms...`);
77
- this.reconnectTimer = setTimeout(() => {
114
+ this.reconnectAttempt += 1;
115
+ console.log(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
116
+ this.reconnectTimer = this.clock.setTimeout(() => {
78
117
  this.reconnectTimer = null;
79
118
  this.doConnect();
80
119
  }, this.reconnectDelay);
81
120
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
82
121
  }
122
+ resetWatchdog() {
123
+ this.clearWatchdog();
124
+ const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
125
+ this.watchdogTimer = this.clock.setTimeout(() => {
126
+ console.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
127
+ try {
128
+ this.ws?.terminate();
129
+ } catch {
130
+ }
131
+ }, ms);
132
+ }
133
+ clearWatchdog() {
134
+ if (this.watchdogTimer) {
135
+ this.clock.clearTimeout(this.watchdogTimer);
136
+ this.watchdogTimer = null;
137
+ }
138
+ }
83
139
  };
84
140
 
85
141
  // src/agentProcessManager.ts
@@ -200,30 +256,29 @@ Each channel has a **name** and optionally a **description** that define its pur
200
256
 
201
257
  \`read_history(channel="#channel-name")\` or \`read_history(channel="dm:@peer-name")\` or \`read_history(channel="#channel:shortid")\`
202
258
 
203
- ### Task boards
259
+ ### Tasks
204
260
 
205
- Each channel has a task board with two independent dimensions: **status** (progress) and **assignee** (who's doing it).
261
+ 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
262
 
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.
263
+ **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
264
 
213
- **Assignee** is independent from status \u2014 you can claim/unclaim at any status (except done).
265
+ **What you see in messages:**
266
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
267
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
268
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
214
269
 
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.
270
+ \`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
271
 
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.
272
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
223
273
 
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.
274
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
225
275
 
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.
276
+ **Workflow:**
277
+ 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)
278
+ 2. If the claim fails, someone else is working on it \u2014 do not start, move on
279
+ 3. Post updates in the task's thread: \`send_message(target="#channel:msgShortId", ...)\`
280
+ 4. When done, set status to \`in_review\` so a human can validate
281
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
227
282
 
228
283
  ### Splitting tasks for parallel execution
229
284
 
@@ -261,7 +316,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
261
316
 
262
317
  ### Formatting \u2014 No HTML
263
318
 
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.
319
+ 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
320
 
266
321
  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
322
 
@@ -563,13 +618,13 @@ var ClaudeDriver = class {
563
618
  if (name === "mcp__chat__create_tasks") return input.channel || "";
564
619
  if (name === "mcp__chat__claim_tasks") {
565
620
  const nums = input.task_numbers;
566
- return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
621
+ return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
567
622
  }
568
623
  if (name === "mcp__chat__unclaim_task") {
569
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
624
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
570
625
  }
571
626
  if (name === "mcp__chat__update_task_status") {
572
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
627
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
573
628
  }
574
629
  if (name === "mcp__chat__upload_file") return input.file_path || "";
575
630
  return "";
@@ -807,13 +862,13 @@ var CodexDriver = class {
807
862
  if (name === `${this.mcpToolPrefix}create_tasks`) return input.channel || "";
808
863
  if (name === `${this.mcpToolPrefix}claim_tasks`) {
809
864
  const nums = input.task_numbers;
810
- return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
865
+ return input.channel ? `${input.channel} #${Array.isArray(nums) ? nums.join(",#") : nums}` : "";
811
866
  }
812
867
  if (name === `${this.mcpToolPrefix}unclaim_task`) {
813
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
868
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
814
869
  }
815
870
  if (name === `${this.mcpToolPrefix}update_task_status`) {
816
- return input.channel ? `${input.channel} #t${input.task_number}` : "";
871
+ return input.channel ? `${input.channel} #${input.task_number}` : "";
817
872
  }
818
873
  if (name === `${this.mcpToolPrefix}upload_file`) return input.file_path || "";
819
874
  return "";
@@ -858,18 +913,26 @@ function buildUnreadSummary(messages, excludeChannel) {
858
913
  }
859
914
  var MAX_TRAJECTORY_TEXT = 2e3;
860
915
  var ACTIVITY_HEARTBEAT_MS = 6e4;
916
+ var MAX_STDOUT_LINES = 8;
917
+ var MAX_STDOUT_LINE_LENGTH = 240;
861
918
  var MAX_STDERR_LINES = 8;
862
919
  var MAX_STDERR_LINE_LENGTH = 240;
863
- function pushRecentStderr(lines, chunk) {
920
+ function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
864
921
  const next = [...lines];
865
922
  for (const rawLine of chunk.split(/\r?\n/)) {
866
923
  const text = rawLine.trim();
867
924
  if (!text) continue;
868
925
  next.push(
869
- text.length > MAX_STDERR_LINE_LENGTH ? `${text.slice(0, MAX_STDERR_LINE_LENGTH)}...` : text
926
+ text.length > maxLineLength ? `${text.slice(0, maxLineLength)}...` : text
870
927
  );
871
928
  }
872
- return next.slice(-MAX_STDERR_LINES);
929
+ return next.slice(-maxLines);
930
+ }
931
+ function pushRecentStderr(lines, chunk) {
932
+ return pushRecentLines(lines, chunk, MAX_STDERR_LINES, MAX_STDERR_LINE_LENGTH);
933
+ }
934
+ function pushRecentStdout(lines, chunk) {
935
+ return pushRecentLines(lines, chunk, MAX_STDOUT_LINES, MAX_STDOUT_LINE_LENGTH);
873
936
  }
874
937
  function formatCrashReason(code, signal, ap) {
875
938
  const parts = [];
@@ -889,6 +952,9 @@ function formatCrashReason(code, signal, ap) {
889
952
  if (ap.recentStderr.length > 0) {
890
953
  parts.push(`stderr: ${ap.recentStderr.join(" | ")}`);
891
954
  }
955
+ if (!ap.lastRuntimeError && ap.recentStdout.length > 0) {
956
+ parts.push(`stdout: ${ap.recentStdout.join(" | ")}`);
957
+ }
892
958
  return parts.join(" | ");
893
959
  }
894
960
  function summarizeCrash(code, signal) {
@@ -924,7 +990,14 @@ var AgentProcessManager = class _AgentProcessManager {
924
990
  this.driverResolver = opts.driverResolver || getDriver;
925
991
  }
926
992
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt) {
927
- if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
993
+ if (this.agents.has(agentId)) {
994
+ console.log(`[Agent ${agentId}] Start ignored (already running)`);
995
+ return;
996
+ }
997
+ if (this.agentsStarting.has(agentId)) {
998
+ console.log(`[Agent ${agentId}] Start ignored (startup in progress)`);
999
+ return;
1000
+ }
928
1001
  this.agentsStarting.add(agentId);
929
1002
  try {
930
1003
  const driver = this.driverResolver(config.runtime || "claude");
@@ -1030,6 +1103,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1030
1103
  activityHeartbeat: null,
1031
1104
  lastActivity: "",
1032
1105
  lastActivityDetail: "",
1106
+ recentStdout: [],
1033
1107
  recentStderr: [],
1034
1108
  lastRuntimeError: null,
1035
1109
  spawnError: null,
@@ -1041,7 +1115,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1041
1115
  this.agentsStarting.delete(agentId);
1042
1116
  let buffer = "";
1043
1117
  proc.stdout?.on("data", (chunk) => {
1044
- buffer += chunk.toString();
1118
+ const chunkText = chunk.toString();
1119
+ const current = this.agents.get(agentId);
1120
+ if (current) {
1121
+ current.recentStdout = pushRecentStdout(current.recentStdout, chunkText);
1122
+ }
1123
+ buffer += chunkText;
1045
1124
  const lines = buffer.split("\n");
1046
1125
  buffer = lines.pop() || "";
1047
1126
  for (const line of lines) {
@@ -1092,6 +1171,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1092
1171
  const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
1093
1172
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
1094
1173
  if (queuedWakeMessage) {
1174
+ console.log(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
1095
1175
  const nextConfig = { ...ap.config, sessionId: ap.sessionId };
1096
1176
  this.idleAgentConfigs.set(agentId, {
1097
1177
  config: nextConfig,
@@ -1113,6 +1193,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1113
1193
  config: { ...ap.config, sessionId: ap.sessionId },
1114
1194
  sessionId: ap.sessionId
1115
1195
  });
1196
+ if (!ap.driver.supportsStdinNotification) {
1197
+ console.log(`[Agent ${agentId}] Turn completed; cached idle state for future restart`);
1198
+ }
1116
1199
  this.broadcastActivity(agentId, "online", "Process idle");
1117
1200
  } else {
1118
1201
  this.idleAgentConfigs.delete(agentId);
@@ -1153,7 +1236,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1153
1236
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
1154
1237
  this.idleAgentConfigs.delete(agentId);
1155
1238
  const ap = this.agents.get(agentId);
1156
- if (!ap) return;
1239
+ if (!ap) {
1240
+ if (!silent) {
1241
+ console.log(`[Agent ${agentId}] Stop requested but no running process was found`);
1242
+ }
1243
+ return;
1244
+ }
1157
1245
  if (ap.notificationTimer) {
1158
1246
  clearTimeout(ap.notificationTimer);
1159
1247
  }
@@ -1165,10 +1253,14 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1165
1253
  if (!silent) {
1166
1254
  this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1167
1255
  this.broadcastActivity(agentId, "offline", "Stopped");
1256
+ console.log(`[Agent ${agentId}] Stopped by request`);
1168
1257
  }
1169
1258
  if (wait) {
1170
1259
  await new Promise((resolve) => {
1171
1260
  const forceKillTimer = setTimeout(() => {
1261
+ if (!silent) {
1262
+ console.warn(`[Agent ${agentId}] Stop timed out; force killing`);
1263
+ }
1172
1264
  try {
1173
1265
  ap.process.kill("SIGKILL");
1174
1266
  } catch {
@@ -1197,7 +1289,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1197
1289
  }
1198
1290
  const cached = this.idleAgentConfigs.get(agentId);
1199
1291
  if (cached) {
1200
- console.log(`[Agent ${agentId}] Auto-restarting idle process for incoming message`);
1292
+ console.log(`[Agent ${agentId}] Starting from idle state for new message`);
1201
1293
  this.idleAgentConfigs.delete(agentId);
1202
1294
  this.startAgent(agentId, cached.config, message).catch((err) => {
1203
1295
  console.error(`[Agent ${agentId}] Failed to auto-restart:`, err);
@@ -1225,9 +1317,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1225
1317
  const agentDataDir = path3.join(this.dataDir, agentId);
1226
1318
  try {
1227
1319
  await rm(agentDataDir, { recursive: true, force: true });
1228
- console.log(`[Agent ${agentId}] Workspace deleted: ${agentDataDir}`);
1320
+ console.log(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
1229
1321
  } catch (err) {
1230
- console.error(`[Agent ${agentId}] Failed to delete workspace:`, err);
1322
+ console.error(`[Agent ${agentId}] Workspace reset failed:`, err);
1231
1323
  }
1232
1324
  }
1233
1325
  async stopAll() {
@@ -1238,6 +1330,19 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1238
1330
  getRunningAgentIds() {
1239
1331
  return [...this.agents.keys()];
1240
1332
  }
1333
+ getDataDir() {
1334
+ return this.dataDir;
1335
+ }
1336
+ getAgentSessionId(agentId) {
1337
+ return this.agents.get(agentId)?.sessionId ?? null;
1338
+ }
1339
+ getIdleAgentSessionIds() {
1340
+ const result = [];
1341
+ for (const [agentId, { sessionId }] of this.idleAgentConfigs) {
1342
+ if (sessionId) result.push({ agentId, sessionId });
1343
+ }
1344
+ return result;
1345
+ }
1241
1346
  // Machine-level workspace scanning
1242
1347
  async scanAllWorkspaces() {
1243
1348
  const results = [];
@@ -1482,6 +1587,11 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1482
1587
  case "session_init":
1483
1588
  if (ap) ap.sessionId = event.sessionId;
1484
1589
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1590
+ writeFile(
1591
+ path3.join(this.dataDir, agentId, "session-meta.json"),
1592
+ JSON.stringify({ sessionId: event.sessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
1593
+ ).catch(() => {
1594
+ });
1485
1595
  break;
1486
1596
  case "thinking": {
1487
1597
  const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
@@ -1518,6 +1628,11 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1518
1628
  }
1519
1629
  if (event.sessionId) {
1520
1630
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1631
+ writeFile(
1632
+ path3.join(this.dataDir, agentId, "session-meta.json"),
1633
+ JSON.stringify({ sessionId: event.sessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
1634
+ ).catch(() => {
1635
+ });
1521
1636
  }
1522
1637
  break;
1523
1638
  case "error": {
@@ -1652,6 +1767,74 @@ var RUNTIMES = [
1652
1767
  // src/index.ts
1653
1768
  var require2 = createRequire(import.meta.url);
1654
1769
  var DAEMON_VERSION = require2("../package.json").version;
1770
+ var LOG_DIR = path4.join(os2.homedir(), ".slock", "logs");
1771
+ var LOG_FILE = path4.join(LOG_DIR, "daemon.log");
1772
+ var MAX_LOG_BYTES = 5 * 1024 * 1024;
1773
+ async function initLogFile() {
1774
+ try {
1775
+ await mkdir2(LOG_DIR, { recursive: true });
1776
+ try {
1777
+ const s = await stat2(LOG_FILE);
1778
+ if (s.size > MAX_LOG_BYTES) {
1779
+ const content = await readFile2(LOG_FILE, "utf-8");
1780
+ const trimmed = content.slice(Math.floor(content.length / 2));
1781
+ await appendFile(LOG_FILE, "");
1782
+ const { writeFile: writeFile2 } = await import("fs/promises");
1783
+ await writeFile2(LOG_FILE, trimmed);
1784
+ }
1785
+ } catch {
1786
+ }
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ function logLine(level, ...args2) {
1791
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args2.map(String).join(" ")}
1792
+ `;
1793
+ appendFile(LOG_FILE, line).catch(() => {
1794
+ });
1795
+ }
1796
+ var _origLog = console.log.bind(console);
1797
+ var _origErr = console.error.bind(console);
1798
+ var _origWarn = console.warn.bind(console);
1799
+ console.log = (...args2) => {
1800
+ _origLog(...args2);
1801
+ logLine("INFO", ...args2);
1802
+ };
1803
+ console.error = (...args2) => {
1804
+ _origErr(...args2);
1805
+ logLine("ERROR", ...args2);
1806
+ };
1807
+ console.warn = (...args2) => {
1808
+ _origWarn(...args2);
1809
+ logLine("WARN", ...args2);
1810
+ };
1811
+ function formatChannelTarget(msg) {
1812
+ return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
1813
+ }
1814
+ function summarizeIncomingMessage(msg) {
1815
+ switch (msg.type) {
1816
+ case "agent:start":
1817
+ return `(agent=${msg.agentId}, runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`;
1818
+ case "agent:stop":
1819
+ return `(agent=${msg.agentId})`;
1820
+ case "agent:reset-workspace":
1821
+ return `(agent=${msg.agentId})`;
1822
+ case "agent:deliver":
1823
+ return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
1824
+ case "agent:workspace:list":
1825
+ return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
1826
+ case "agent:workspace:read":
1827
+ return `(agent=${msg.agentId}, path=${msg.path})`;
1828
+ case "agent:skills:list":
1829
+ return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
1830
+ case "machine:workspace:delete":
1831
+ return `(directory=${msg.directoryName})`;
1832
+ case "machine:feedback:collect":
1833
+ return `(reportId=${msg.reportId}, agents=${msg.agents.length})`;
1834
+ default:
1835
+ return "";
1836
+ }
1837
+ }
1655
1838
  function detectRuntimes() {
1656
1839
  const detected = [];
1657
1840
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -1683,6 +1866,126 @@ try {
1683
1866
  chatBridgePath = path4.resolve(__dirname, "chat-bridge.ts");
1684
1867
  }
1685
1868
  var connection;
1869
+ async function collectAndUploadAgent(opts) {
1870
+ const { agentId, reportAgentId, uploadUrl, authToken, timeRangeHours, includeSessionFiles, includeDaemonLogs, includeWorkspaceSnapshot, dataDir } = opts;
1871
+ const sinceMs = Date.now() - timeRangeHours * 60 * 60 * 1e3;
1872
+ let bytesUploaded = 0;
1873
+ let anyUploadFailed = false;
1874
+ try {
1875
+ const agentDir = path4.join(dataDir, agentId);
1876
+ let sessionId = opts.sessionId;
1877
+ try {
1878
+ const meta = JSON.parse(await readFile2(path4.join(agentDir, "session-meta.json"), "utf-8"));
1879
+ if (meta.sessionId) sessionId = meta.sessionId;
1880
+ } catch {
1881
+ }
1882
+ const filesToUpload = [];
1883
+ if (includeSessionFiles && sessionId) {
1884
+ const claudeProjectsDir = path4.join(os2.homedir(), ".claude", "projects");
1885
+ try {
1886
+ const projectDirs = await readdir2(claudeProjectsDir, { withFileTypes: true });
1887
+ for (const pd of projectDirs) {
1888
+ if (!pd.isDirectory()) continue;
1889
+ const pdPath = path4.join(claudeProjectsDir, pd.name);
1890
+ const files = await readdir2(pdPath, { withFileTypes: true });
1891
+ for (const f of files) {
1892
+ if (!f.isFile() || !f.name.endsWith(".jsonl")) continue;
1893
+ if (!f.name.startsWith(sessionId)) continue;
1894
+ const filePath = path4.join(pdPath, f.name);
1895
+ const raw = await readFile2(filePath, "utf-8");
1896
+ const filteredLines = raw.split("\n").filter((line) => {
1897
+ if (!line.trim()) return false;
1898
+ try {
1899
+ const obj = JSON.parse(line);
1900
+ if (!obj.timestamp) return true;
1901
+ return new Date(obj.timestamp).getTime() >= sinceMs;
1902
+ } catch {
1903
+ return false;
1904
+ }
1905
+ }).join("\n");
1906
+ if (!filteredLines.trim()) continue;
1907
+ filesToUpload.push({ filename: f.name, data: Buffer.from(filteredLines), kind: "session_jsonl" });
1908
+ }
1909
+ }
1910
+ } catch {
1911
+ }
1912
+ }
1913
+ if (includeDaemonLogs) {
1914
+ try {
1915
+ const raw = await readFile2(LOG_FILE, "utf-8");
1916
+ const filtered = raw.split("\n").filter((line) => {
1917
+ if (!line.trim()) return false;
1918
+ const ts = line.slice(0, 24);
1919
+ return new Date(ts).getTime() >= sinceMs;
1920
+ }).join("\n");
1921
+ if (filtered.trim()) {
1922
+ filesToUpload.push({ filename: "daemon.log", data: Buffer.from(filtered), kind: "daemon_log" });
1923
+ }
1924
+ } catch {
1925
+ }
1926
+ }
1927
+ if (includeWorkspaceSnapshot) {
1928
+ try {
1929
+ const agentStat = await stat2(agentDir);
1930
+ if (agentStat.isDirectory()) {
1931
+ const files = await readdir2(agentDir, { withFileTypes: true });
1932
+ for (const f of files) {
1933
+ if (!f.isFile()) continue;
1934
+ if (f.name === "session-meta.json") continue;
1935
+ const filePath = path4.join(agentDir, f.name);
1936
+ const s = await stat2(filePath);
1937
+ if (s.size > 1024 * 1024) continue;
1938
+ const data = await readFile2(filePath);
1939
+ filesToUpload.push({ filename: f.name, data, kind: "runtime_log" });
1940
+ }
1941
+ }
1942
+ } catch {
1943
+ }
1944
+ }
1945
+ const manifest = {
1946
+ reportAgentId,
1947
+ agentId,
1948
+ sessionId,
1949
+ runtime: opts.runtime,
1950
+ daemonVersion: DAEMON_VERSION,
1951
+ hostname: os2.hostname(),
1952
+ os: `${os2.platform()} ${os2.arch()}`,
1953
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1954
+ timeRangeHours,
1955
+ includeSessionFiles,
1956
+ includeDaemonLogs,
1957
+ includeWorkspaceSnapshot,
1958
+ artifactCount: filesToUpload.length
1959
+ };
1960
+ filesToUpload.unshift({ filename: "manifest.json", data: Buffer.from(JSON.stringify(manifest, null, 2)), kind: "manifest" });
1961
+ for (const file of filesToUpload) {
1962
+ const formData = new FormData();
1963
+ formData.append("reportAgentId", reportAgentId);
1964
+ formData.append("kind", file.kind);
1965
+ formData.append("filename", file.filename);
1966
+ formData.append("file", new Blob([new Uint8Array(file.data)]), file.filename);
1967
+ const resp = await fetch(uploadUrl, {
1968
+ method: "POST",
1969
+ headers: { Authorization: `Bearer ${authToken}` },
1970
+ body: formData
1971
+ });
1972
+ if (resp.ok) {
1973
+ bytesUploaded += file.data.length;
1974
+ } else {
1975
+ console.warn(`[Feedback] Upload failed for ${file.filename}: ${resp.status}`);
1976
+ anyUploadFailed = true;
1977
+ }
1978
+ }
1979
+ if (anyUploadFailed) {
1980
+ return { reportAgentId, status: "error", daemonVersion: DAEMON_VERSION, error: "One or more files failed to upload", bytesUploaded };
1981
+ }
1982
+ return { reportAgentId, status: "collected", daemonVersion: DAEMON_VERSION, bytesUploaded };
1983
+ } catch (err) {
1984
+ const error = err instanceof Error ? err.message : String(err);
1985
+ console.error(`[Feedback] Collection failed for agent ${agentId}:`, error);
1986
+ return { reportAgentId, status: "error", daemonVersion: DAEMON_VERSION, error };
1987
+ }
1988
+ }
1686
1989
  var agentManager = new AgentProcessManager(chatBridgePath, (msg) => {
1687
1990
  connection.send(msg);
1688
1991
  }, apiKey);
@@ -1690,27 +1993,28 @@ connection = new DaemonConnection({
1690
1993
  serverUrl,
1691
1994
  apiKey,
1692
1995
  onMessage: (msg) => {
1693
- console.log(`[Daemon] Received: ${msg.type}`, msg.type === "ping" ? "" : JSON.stringify(msg).slice(0, 200));
1996
+ const summary = summarizeIncomingMessage(msg);
1997
+ console.log(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
1694
1998
  switch (msg.type) {
1695
1999
  case "agent:start":
1696
- console.log(`[Daemon] Starting agent ${msg.agentId} (model: ${msg.config.model}, session: ${msg.config.sessionId || "new"}${msg.wakeMessage ? ", with wake message" : ""})`);
2000
+ console.log(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
1697
2001
  agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt).catch((err) => {
1698
2002
  const reason = err instanceof Error ? err.message : String(err);
1699
- console.error(`[Daemon] Failed to start agent ${msg.agentId}:`, reason);
2003
+ console.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
1700
2004
  connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive" });
1701
2005
  connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}` });
1702
2006
  });
1703
2007
  break;
1704
2008
  case "agent:stop":
1705
- console.log(`[Daemon] Stopping agent ${msg.agentId}`);
2009
+ console.log(`[Agent ${msg.agentId}] Stop requested`);
1706
2010
  agentManager.stopAgent(msg.agentId);
1707
2011
  break;
1708
2012
  case "agent:reset-workspace":
1709
- console.log(`[Daemon] Resetting workspace for agent ${msg.agentId}`);
2013
+ console.log(`[Agent ${msg.agentId}] Workspace reset requested`);
1710
2014
  agentManager.resetWorkspace(msg.agentId);
1711
2015
  break;
1712
2016
  case "agent:deliver":
1713
- console.log(`[Daemon] Delivering message to ${msg.agentId}: ${msg.message.content.slice(0, 80)}`);
2017
+ console.log(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
1714
2018
  agentManager.deliverMessage(msg.agentId, msg.message);
1715
2019
  connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
1716
2020
  break;
@@ -1761,6 +2065,37 @@ connection = new DaemonConnection({
1761
2065
  case "ping":
1762
2066
  connection.send({ type: "pong" });
1763
2067
  break;
2068
+ case "machine:feedback:collect": {
2069
+ const { reportId, agents, timeRangeHours, includeSessionFiles, includeDaemonLogs, includeWorkspaceSnapshot, uploadUrl, authToken } = msg;
2070
+ console.log(`[Daemon] Collecting feedback for report ${reportId} (${agents.length} agents)`);
2071
+ const dataDir = agentManager.getDataDir();
2072
+ Promise.all(agents.map(
2073
+ (agent) => collectAndUploadAgent({
2074
+ agentId: agent.agentId,
2075
+ reportAgentId: agent.reportAgentId,
2076
+ runtime: agent.runtime,
2077
+ sessionId: agent.sessionId,
2078
+ uploadUrl,
2079
+ authToken,
2080
+ timeRangeHours,
2081
+ includeSessionFiles,
2082
+ includeDaemonLogs,
2083
+ includeWorkspaceSnapshot: includeWorkspaceSnapshot ?? false,
2084
+ dataDir
2085
+ })
2086
+ )).then((agentResults) => {
2087
+ connection.send({ type: "machine:feedback:result", reportId, agentResults });
2088
+ }).catch((err) => {
2089
+ console.error(`[Daemon] Feedback collection failed for report ${reportId}:`, err);
2090
+ const agentResults = agents.map((a) => ({
2091
+ reportAgentId: a.reportAgentId,
2092
+ status: "error",
2093
+ error: err instanceof Error ? err.message : String(err)
2094
+ }));
2095
+ connection.send({ type: "machine:feedback:result", reportId, agentResults });
2096
+ });
2097
+ break;
2098
+ }
1764
2099
  }
1765
2100
  },
1766
2101
  onConnect: () => {
@@ -1775,11 +2110,22 @@ connection = new DaemonConnection({
1775
2110
  os: `${os2.platform()} ${os2.arch()}`,
1776
2111
  daemonVersion: DAEMON_VERSION
1777
2112
  });
2113
+ for (const agentId of agentManager.getRunningAgentIds()) {
2114
+ const sessionId = agentManager.getAgentSessionId(agentId);
2115
+ if (sessionId) {
2116
+ connection.send({ type: "agent:session", agentId, sessionId });
2117
+ }
2118
+ }
2119
+ for (const { agentId, sessionId } of agentManager.getIdleAgentSessionIds()) {
2120
+ connection.send({ type: "agent:session", agentId, sessionId });
2121
+ }
1778
2122
  },
1779
2123
  onDisconnect: () => {
1780
2124
  console.log("[Daemon] Lost connection \u2014 agents continue running locally");
1781
2125
  }
1782
2126
  });
2127
+ initLogFile().catch(() => {
2128
+ });
1783
2129
  console.log("[Slock Daemon] Starting...");
1784
2130
  connection.connect();
1785
2131
  var shutdown = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.28.1-alpha.3",
3
+ "version": "0.29.1-alpha.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"
@@ -32,7 +32,7 @@
32
32
  "dev": "tsx watch src/index.ts",
33
33
  "start": "tsx src/index.ts",
34
34
  "build": "tsup",
35
- "test": "node --import tsx --test src/**/*.test.ts",
35
+ "test": "node --import tsx --test --test-force-exit 'src/**/*.test.ts'",
36
36
  "typecheck": "tsc --noEmit",
37
37
  "release:patch": "npm version patch --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",
38
38
  "release:minor": "npm version minor --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",