@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.
- package/dist/chat-bridge.js +70 -21
- package/dist/index.js +396 -50
- package/package.json +2 -2
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
|
@@ -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
|
-
|
|
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
|
-
|
|
51
|
-
this.ws
|
|
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.
|
|
80
|
+
this.reconnectAttempt = 0;
|
|
81
|
+
this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
|
|
82
|
+
this.resetWatchdog();
|
|
54
83
|
this.options.onConnect();
|
|
55
84
|
});
|
|
56
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
###
|
|
259
|
+
### Tasks
|
|
204
260
|
|
|
205
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
272
|
+
**Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
|
|
223
273
|
|
|
224
|
-
**
|
|
274
|
+
**Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
|
|
225
275
|
|
|
226
|
-
**
|
|
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\`, \`#
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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 >
|
|
926
|
+
text.length > maxLineLength ? `${text.slice(0, maxLineLength)}...` : text
|
|
870
927
|
);
|
|
871
928
|
}
|
|
872
|
-
return next.slice(-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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}]
|
|
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
|
|
1320
|
+
console.log(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
|
|
1229
1321
|
} catch (err) {
|
|
1230
|
-
console.error(`[Agent ${agentId}]
|
|
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
|
-
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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.
|
|
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",
|