@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.
- package/dist/chat-bridge.js +70 -21
- package/dist/index.js +142 -42
- package/package.json +1 -1
package/dist/chat-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
500
|
-
task_numbers: z.array(z.number()).describe("Task numbers to claim (e.g. [1, 3
|
|
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(
|
|
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
|
-
|
|
558
|
+
const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
|
|
559
|
+
return `${label} (msg:${msgShort}): claimed`;
|
|
521
560
|
}
|
|
522
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
217
|
+
### Tasks
|
|
204
218
|
|
|
205
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
230
|
+
**Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
|
|
223
231
|
|
|
224
|
-
**
|
|
232
|
+
**Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
|
|
225
233
|
|
|
226
|
-
**
|
|
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\`, \`#
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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 >
|
|
884
|
+
text.length > maxLineLength ? `${text.slice(0, maxLineLength)}...` : text
|
|
870
885
|
);
|
|
871
886
|
}
|
|
872
|
-
return next.slice(-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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}]
|
|
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
|
|
1278
|
+
console.log(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
|
|
1205
1279
|
} catch (err) {
|
|
1206
|
-
console.error(`[Agent ${agentId}]
|
|
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
|
-
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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;
|