@slock-ai/daemon 0.30.0 → 0.30.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.
@@ -470,7 +470,7 @@ ${formatted}`
470
470
  );
471
471
  server.tool(
472
472
  "create_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.",
473
+ "Create one or more new task-messages in a top-level channel or DM. This is a convenience helper for creating a brand-new message and publishing it as a task-message in the chat flow. Thread messages cannot become tasks. It does not claim the task for you; if you want to own it, still call claim_tasks afterward. It is not a separate task board outside the chat flow. Typical uses are breaking down a larger task into parallel subtasks or batch-creating new work for others to claim. Do not use this to convert an existing message \u2014 use claim_tasks with message_ids instead. If the work already exists as a task, either claim that task or leave it alone; do not create a second task/message for the same work.",
474
474
  {
475
475
  channel: z.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
476
476
  tasks: z.array(
@@ -519,13 +519,13 @@ server.tool(
519
519
  "claim_tasks",
520
520
  `Claim tasks so you are assigned to work on them. Two modes:
521
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.
522
+ 2. By message ID: convert a regular top-level 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
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.`,
524
+ Thread messages cannot be claimed or converted into tasks. 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.`,
525
525
  {
526
526
  channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
527
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.")
528
+ message_ids: z.array(z.string()).optional().describe("Message IDs or short ID prefixes (the 8-char msg= value, e.g. ['a1b2c3d4']). Converts a regular top-level message to a task and claims it. Thread messages are not allowed.")
529
529
  },
530
530
  async ({ channel, task_numbers, message_ids }) => {
531
531
  try {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import path4 from "path";
4
+ import path5 from "path";
5
5
  import os2 from "os";
6
6
  import { createRequire } from "module";
7
7
  import { execSync as execSync2 } from "child_process";
@@ -10,6 +10,33 @@ import { fileURLToPath } from "url";
10
10
 
11
11
  // src/connection.ts
12
12
  import WebSocket from "ws";
13
+
14
+ // src/logger.ts
15
+ function timestamp() {
16
+ const d = /* @__PURE__ */ new Date();
17
+ const pad = (n) => String(n).padStart(2, "0");
18
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
19
+ }
20
+ function format(level, msg) {
21
+ return `${timestamp()} [${level}] ${msg}`;
22
+ }
23
+ var logger = {
24
+ info(msg) {
25
+ console.log(format("INFO", msg));
26
+ },
27
+ warn(msg) {
28
+ console.warn(format("WARN", msg));
29
+ },
30
+ error(msg, err) {
31
+ if (err) {
32
+ console.error(format("ERROR", msg), err);
33
+ } else {
34
+ console.error(format("ERROR", msg));
35
+ }
36
+ }
37
+ };
38
+
39
+ // src/connection.ts
13
40
  var systemClock = {
14
41
  now: () => Date.now(),
15
42
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -46,7 +73,7 @@ var DaemonConnection = class {
46
73
  this.reconnectTimer = null;
47
74
  }
48
75
  if (this.ws) {
49
- console.log("[Daemon] Disconnect requested");
76
+ logger.info("[Daemon] Disconnect requested");
50
77
  this.ws.close();
51
78
  this.ws = null;
52
79
  }
@@ -59,7 +86,7 @@ var DaemonConnection = class {
59
86
  const now = this.clock.now();
60
87
  if (now - this.lastDroppedSendLogAt > 5e3) {
61
88
  this.lastDroppedSendLogAt = now;
62
- console.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
89
+ logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
63
90
  }
64
91
  }
65
92
  get connected() {
@@ -69,13 +96,13 @@ var DaemonConnection = class {
69
96
  if (!this.shouldConnect) return;
70
97
  if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
71
98
  const wsUrl = this.options.serverUrl.replace(/^http/, "ws") + `/daemon/connect?key=${this.options.apiKey}`;
72
- console.log(`[Daemon] Connecting to ${this.options.serverUrl}...`);
99
+ logger.info(`[Daemon] Connecting to ${this.options.serverUrl}...`);
73
100
  const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl) : new WebSocket(wsUrl);
74
101
  this.ws = ws;
75
102
  ws.on("open", () => {
76
103
  if (this.ws !== ws) return;
77
104
  if (!this.shouldConnect) return;
78
- console.log("[Daemon] Connected to server");
105
+ logger.info("[Daemon] Connected to server");
79
106
  this.reconnectAttempt = 0;
80
107
  this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
81
108
  this.resetWatchdog();
@@ -88,7 +115,7 @@ var DaemonConnection = class {
88
115
  const msg = JSON.parse(data.toString());
89
116
  this.options.onMessage(msg);
90
117
  } catch (err) {
91
- console.error("[Daemon] Invalid message from server:", err);
118
+ logger.error("[Daemon] Invalid message from server", err);
92
119
  }
93
120
  });
94
121
  ws.on("close", (code, reasonBuffer) => {
@@ -96,7 +123,7 @@ var DaemonConnection = class {
96
123
  this.ws = null;
97
124
  this.clearWatchdog();
98
125
  const reason = reasonBuffer.toString("utf8");
99
- console.log(
126
+ logger.warn(
100
127
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
101
128
  );
102
129
  this.options.onDisconnect();
@@ -104,14 +131,14 @@ var DaemonConnection = class {
104
131
  });
105
132
  ws.on("error", (err) => {
106
133
  if (this.ws !== ws) return;
107
- console.error("[Daemon] WebSocket error:", err.message);
134
+ logger.error(`[Daemon] WebSocket error: ${err.message}`);
108
135
  });
109
136
  }
110
137
  scheduleReconnect() {
111
138
  if (!this.shouldConnect) return;
112
139
  if (this.reconnectTimer) return;
113
140
  this.reconnectAttempt += 1;
114
- console.log(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
141
+ logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
115
142
  this.reconnectTimer = this.clock.setTimeout(() => {
116
143
  this.reconnectTimer = null;
117
144
  this.doConnect();
@@ -122,7 +149,7 @@ var DaemonConnection = class {
122
149
  this.clearWatchdog();
123
150
  const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
124
151
  this.watchdogTimer = this.clock.setTimeout(() => {
125
- console.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
152
+ logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
126
153
  try {
127
154
  this.ws?.terminate();
128
155
  } catch {
@@ -139,7 +166,7 @@ var DaemonConnection = class {
139
166
 
140
167
  // src/agentProcessManager.ts
141
168
  import { mkdir, writeFile, access, readdir, stat, readFile, rm } from "fs/promises";
142
- import path3 from "path";
169
+ import path4 from "path";
143
170
  import os from "os";
144
171
 
145
172
  // src/drivers/claude.ts
@@ -182,7 +209,7 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
182
209
  3. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
183
210
  4. **${t("read_history")}** \u2014 Read past messages from a channel or DM.
184
211
  5. **${t("list_tasks")}** \u2014 View a channel's task board.
185
- 6. **${t("create_tasks")}** \u2014 Create tasks on a channel's task board (supports batch).
212
+ 6. **${t("create_tasks")}** \u2014 Create new task-messages in a channel (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
186
213
  7. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
187
214
  8. **${t("unclaim_task")}** \u2014 Release your claim on a task.
188
215
  9. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
@@ -266,6 +293,8 @@ When someone sends a message that asks you to do something \u2014 fix a bug, wri
266
293
  - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
267
294
  - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
268
295
 
296
+ Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context: reply there, but do not claim or convert them as tasks.
297
+
269
298
  \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
270
299
 
271
300
  **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
@@ -279,6 +308,21 @@ When someone sends a message that asks you to do something \u2014 fix a bug, wri
279
308
  4. When done, set status to \`in_review\` so a human can validate
280
309
  5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
281
310
 
311
+ **What \`${t("create_tasks")}\` really means:**
312
+ - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
313
+ - \`${t("create_tasks")}\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
314
+ - \`${t("create_tasks")}\` does NOT claim the new task for you. If you want to own it, still call \`${t("claim_tasks")}\` afterward.
315
+ - Typical uses for \`${t("create_tasks")}\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
316
+ - If someone already sent the small work item as a message, you usually do NOT need \`${t("create_tasks")}\` \u2014 just claim that existing message/task.
317
+ - If the work already exists as a message, do NOT use \`${t("create_tasks")}\` for it. Reuse the existing message via \`${t("claim_tasks")}\` with \`message_ids\`.
318
+
319
+ **Creating new tasks:**
320
+ - The task system exists to prevent duplicate work. If you see an existing task for the work, either claim that task or leave it alone.
321
+ - If a message already shows a \`[task #N ...]\` suffix, do NOT create a second message/task for the same work. Claim \`#N\` if it is yours to take; otherwise move on.
322
+ - Before calling \`${t("create_tasks")}\`, first check whether the work already exists as a message-backed task on the task board or is already being handled by someone else.
323
+ - Do NOT create a second task that merely restates an existing request, message, or task. Reuse the existing task/thread instead.
324
+ - Use \`${t("create_tasks")}\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.
325
+
282
326
  ### Splitting tasks for parallel execution
283
327
 
284
328
  When you need to break down a large task into subtasks, structure them so agents can work **in parallel**:
@@ -552,7 +596,7 @@ var ClaudeDriver = class {
552
596
  }
553
597
  return events;
554
598
  }
555
- encodeStdinMessage(text, sessionId) {
599
+ encodeStdinMessage(text, sessionId, _opts) {
556
600
  return JSON.stringify({
557
601
  type: "user",
558
602
  message: {
@@ -807,7 +851,7 @@ var CodexDriver = class {
807
851
  }
808
852
  return events;
809
853
  }
810
- encodeStdinMessage(_text, _sessionId) {
854
+ encodeStdinMessage(_text, _sessionId, _opts) {
811
855
  return null;
812
856
  }
813
857
  buildSystemPrompt(config, _agentId) {
@@ -877,21 +921,247 @@ var CodexDriver = class {
877
921
  }
878
922
  };
879
923
 
924
+ // src/drivers/kimi.ts
925
+ import { randomUUID } from "crypto";
926
+ import { spawn as spawn3 } from "child_process";
927
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
928
+ import path3 from "path";
929
+ var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
930
+ var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
931
+ var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
932
+ var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
933
+ function parseToolArguments(raw) {
934
+ if (typeof raw !== "string") return raw;
935
+ try {
936
+ return JSON.parse(raw);
937
+ } catch {
938
+ return raw;
939
+ }
940
+ }
941
+ var KimiDriver = class {
942
+ id = "kimi";
943
+ supportsStdinNotification = true;
944
+ mcpToolPrefix = "";
945
+ deliverMessageDirectlyWhileBusy = true;
946
+ sessionId = null;
947
+ sessionAnnounced = false;
948
+ promptRequestId = null;
949
+ spawn(ctx) {
950
+ const isResume = !!ctx.config.sessionId;
951
+ this.sessionId = ctx.config.sessionId || randomUUID();
952
+ this.sessionAnnounced = false;
953
+ this.promptRequestId = randomUUID();
954
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
955
+ const command = isTsSource ? "npx" : "node";
956
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
957
+ const systemPromptPath = path3.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
958
+ const agentFilePath = path3.join(ctx.workingDirectory, KIMI_AGENT_FILE);
959
+ const mcpConfigPath = path3.join(ctx.workingDirectory, KIMI_MCP_FILE);
960
+ if (!isResume || !existsSync2(systemPromptPath)) {
961
+ writeFileSync2(systemPromptPath, ctx.prompt, "utf8");
962
+ }
963
+ writeFileSync2(agentFilePath, [
964
+ "version: 1",
965
+ "agent:",
966
+ " extend: default",
967
+ ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
968
+ ""
969
+ ].join("\n"), "utf8");
970
+ writeFileSync2(mcpConfigPath, JSON.stringify({
971
+ mcpServers: {
972
+ chat: {
973
+ command,
974
+ args: bridgeArgs
975
+ }
976
+ }
977
+ }), "utf8");
978
+ const args2 = [
979
+ "--wire",
980
+ "--yolo",
981
+ "--agent-file",
982
+ agentFilePath,
983
+ "--mcp-config-file",
984
+ mcpConfigPath,
985
+ "--session",
986
+ this.sessionId
987
+ ];
988
+ if (ctx.config.model && ctx.config.model !== "default") {
989
+ args2.push("--model", ctx.config.model);
990
+ }
991
+ const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
992
+ const proc = spawn3("kimi", args2, {
993
+ cwd: ctx.workingDirectory,
994
+ stdio: ["pipe", "pipe", "pipe"],
995
+ env: spawnEnv,
996
+ shell: process.platform === "win32"
997
+ });
998
+ proc.stdin?.write(JSON.stringify({
999
+ jsonrpc: "2.0",
1000
+ id: randomUUID(),
1001
+ method: "initialize",
1002
+ params: {
1003
+ protocol_version: KIMI_WIRE_PROTOCOL_VERSION,
1004
+ client: { name: "slock-daemon", version: "1.0.0" },
1005
+ capabilities: {
1006
+ supports_question: false,
1007
+ supports_plan_mode: false
1008
+ }
1009
+ }
1010
+ }) + "\n");
1011
+ proc.stdin?.write(JSON.stringify({
1012
+ jsonrpc: "2.0",
1013
+ id: this.promptRequestId,
1014
+ method: "prompt",
1015
+ params: {
1016
+ user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
1017
+ }
1018
+ }) + "\n");
1019
+ return { process: proc };
1020
+ }
1021
+ parseLine(line) {
1022
+ let message;
1023
+ try {
1024
+ message = JSON.parse(line);
1025
+ } catch {
1026
+ return [];
1027
+ }
1028
+ const events = [];
1029
+ if (!this.sessionAnnounced && this.sessionId) {
1030
+ events.push({ kind: "session_init", sessionId: this.sessionId });
1031
+ this.sessionAnnounced = true;
1032
+ }
1033
+ if ("method" in message && message.method === "event") {
1034
+ const eventType = message.params?.type;
1035
+ const payload = message.params?.payload || {};
1036
+ switch (eventType) {
1037
+ case "StepBegin":
1038
+ events.push({ kind: "thinking", text: "" });
1039
+ break;
1040
+ case "ContentPart":
1041
+ if (payload.type === "think" && payload.think) {
1042
+ events.push({ kind: "thinking", text: payload.think });
1043
+ } else if (payload.type === "text" && payload.text) {
1044
+ events.push({ kind: "text", text: payload.text });
1045
+ }
1046
+ break;
1047
+ case "ToolCall":
1048
+ events.push({
1049
+ kind: "tool_call",
1050
+ name: payload.function?.name || "unknown_tool",
1051
+ input: parseToolArguments(payload.function?.arguments)
1052
+ });
1053
+ break;
1054
+ case "TurnEnd":
1055
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1056
+ break;
1057
+ case "StepInterrupted":
1058
+ events.push({ kind: "error", message: "Turn interrupted" });
1059
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1060
+ break;
1061
+ }
1062
+ return events;
1063
+ }
1064
+ if ("error" in message) {
1065
+ events.push({ kind: "error", message: message.error?.message || "Unknown Kimi error" });
1066
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
1067
+ }
1068
+ return events;
1069
+ }
1070
+ encodeStdinMessage(_text, _sessionId, opts) {
1071
+ const mode = opts?.mode || "busy";
1072
+ if (mode === "idle") {
1073
+ return JSON.stringify({
1074
+ jsonrpc: "2.0",
1075
+ id: randomUUID(),
1076
+ method: "prompt",
1077
+ params: {
1078
+ user_input: _text
1079
+ }
1080
+ });
1081
+ }
1082
+ return JSON.stringify({
1083
+ jsonrpc: "2.0",
1084
+ id: randomUUID(),
1085
+ method: "steer",
1086
+ params: {
1087
+ user_input: _text
1088
+ }
1089
+ });
1090
+ }
1091
+ buildSystemPrompt(config, _agentId) {
1092
+ return buildBaseSystemPrompt(config, {
1093
+ toolPrefix: "",
1094
+ extraCriticalRules: [
1095
+ "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
1096
+ ],
1097
+ postStartupNotes: [],
1098
+ includeStdinNotificationSection: true
1099
+ });
1100
+ }
1101
+ toolDisplayName(name) {
1102
+ if (name === "list_tasks") return "Viewing task board\u2026";
1103
+ if (name === "create_tasks") return "Creating tasks\u2026";
1104
+ if (name === "claim_tasks") return "Claiming tasks\u2026";
1105
+ if (name === "unclaim_task") return "Unclaiming task\u2026";
1106
+ if (name === "update_task_status") return "Updating task\u2026";
1107
+ if (name === "send_message" || name === "receive_message" || name === "read_history" || name === "list_server") return "";
1108
+ if (name === "Shell") return "Running command\u2026";
1109
+ if (name === "ReadFile") return "Reading file\u2026";
1110
+ if (name === "WriteFile" || name === "StrReplaceFile") return "Editing file\u2026";
1111
+ if (name === "Glob" || name === "Grep") return "Searching code\u2026";
1112
+ if (name === "SearchWeb") return "Searching web\u2026";
1113
+ if (name === "FetchURL") return "Fetching web\u2026";
1114
+ if (name === "SetTodoList") return "Updating tasks\u2026";
1115
+ return `Using ${name.length > 20 ? name.slice(0, 20) + "\u2026" : name}\u2026`;
1116
+ }
1117
+ summarizeToolInput(name, input) {
1118
+ if (!input || typeof input !== "object") return "";
1119
+ try {
1120
+ if (name === "Shell") {
1121
+ const cmd = input.command || "";
1122
+ return cmd.length > 100 ? cmd.slice(0, 100) + "\u2026" : cmd;
1123
+ }
1124
+ if (name === "ReadFile" || name === "WriteFile" || name === "StrReplaceFile") {
1125
+ return input.path || "";
1126
+ }
1127
+ if (name === "Glob" || name === "Grep") return input.pattern || input.query || "";
1128
+ if (name === "SearchWeb") return input.query || "";
1129
+ if (name === "FetchURL") return input.url || "";
1130
+ if (name === "send_message") return input.target || input.channel || "";
1131
+ if (name === "read_history") return input.target || input.channel || "";
1132
+ if (name === "list_tasks") return input.channel || "";
1133
+ if (name === "create_tasks") return input.channel || "";
1134
+ if (name === "claim_tasks") {
1135
+ const nums = input.task_numbers;
1136
+ return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
1137
+ }
1138
+ if (name === "unclaim_task" || name === "update_task_status") {
1139
+ return input.channel ? `${input.channel} #t${input.task_number}` : "";
1140
+ }
1141
+ return "";
1142
+ } catch {
1143
+ return "";
1144
+ }
1145
+ }
1146
+ };
1147
+
880
1148
  // src/drivers/index.ts
881
- var drivers = {
882
- claude: new ClaudeDriver(),
883
- codex: new CodexDriver()
1149
+ var driverFactories = {
1150
+ claude: () => new ClaudeDriver(),
1151
+ codex: () => new CodexDriver(),
1152
+ kimi: () => new KimiDriver()
884
1153
  };
885
1154
  function getDriver(runtimeId) {
886
- const driver = drivers[runtimeId];
1155
+ const createDriver = driverFactories[runtimeId];
1156
+ const driver = createDriver?.();
887
1157
  if (!driver) {
888
- throw new Error(`Unknown runtime: ${runtimeId}. Available: ${Object.keys(drivers).join(", ")}`);
1158
+ throw new Error(`Unknown runtime: ${runtimeId}. Available: ${Object.keys(driverFactories).join(", ")}`);
889
1159
  }
890
1160
  return driver;
891
1161
  }
892
1162
 
893
1163
  // src/agentProcessManager.ts
894
- var DATA_DIR = path3.join(os.homedir(), ".slock", "agents");
1164
+ var DATA_DIR = path4.join(os.homedir(), ".slock", "agents");
895
1165
  function toLocalTime(iso) {
896
1166
  const d = new Date(iso);
897
1167
  if (isNaN(d.getTime())) return iso;
@@ -901,6 +1171,28 @@ function toLocalTime(iso) {
901
1171
  function formatChannelLabel(message) {
902
1172
  return message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
903
1173
  }
1174
+ function formatMessageTarget(message) {
1175
+ if (message.channel_type === "thread" && message.parent_channel_name) {
1176
+ const shortId = message.channel_name.startsWith("thread-") ? message.channel_name.slice(7) : message.channel_name;
1177
+ if (message.parent_channel_type === "dm") {
1178
+ return `dm:@${message.parent_channel_name}:${shortId}`;
1179
+ }
1180
+ return `#${message.parent_channel_name}:${shortId}`;
1181
+ }
1182
+ if (message.channel_type === "dm") {
1183
+ return `dm:@${message.channel_name}`;
1184
+ }
1185
+ return `#${message.channel_name}`;
1186
+ }
1187
+ function formatIncomingMessage(message) {
1188
+ const target = formatMessageTarget(message);
1189
+ const msgId = message.message_id ? message.message_id.slice(0, 8) : "-";
1190
+ const time = message.timestamp ? toLocalTime(message.timestamp) : "-";
1191
+ const senderType = message.sender_type === "agent" ? " type=agent" : "";
1192
+ const attachSuffix = message.attachments?.length ? ` [${message.attachments.length} image${message.attachments.length > 1 ? "s" : ""}: ${message.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
1193
+ const taskSuffix = message.task_status ? ` [task #${message.task_number} status=${message.task_status}${message.task_assignee_id ? ` assignee=${message.task_assignee_type}:${message.task_assignee_id}` : ""}]` : "";
1194
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] @${message.sender_name}: ${message.content}${attachSuffix}${taskSuffix}`;
1195
+ }
904
1196
  function buildUnreadSummary(messages, excludeChannel) {
905
1197
  const summary = /* @__PURE__ */ new Map();
906
1198
  for (const message of messages) {
@@ -911,6 +1203,7 @@ function buildUnreadSummary(messages, excludeChannel) {
911
1203
  return summary.size > 0 ? Object.fromEntries(summary) : void 0;
912
1204
  }
913
1205
  var MAX_TRAJECTORY_TEXT = 2e3;
1206
+ var TRAJECTORY_COALESCE_MS = 350;
914
1207
  var ACTIVITY_HEARTBEAT_MS = 6e4;
915
1208
  var MAX_STDOUT_LINES = 8;
916
1209
  var MAX_STDOUT_LINE_LENGTH = 240;
@@ -988,21 +1281,21 @@ var AgentProcessManager = class _AgentProcessManager {
988
1281
  this.dataDir = opts.dataDir || DATA_DIR;
989
1282
  this.driverResolver = opts.driverResolver || getDriver;
990
1283
  }
991
- async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt) {
1284
+ async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
992
1285
  if (this.agents.has(agentId)) {
993
- console.log(`[Agent ${agentId}] Start ignored (already running)`);
1286
+ logger.info(`[Agent ${agentId}] Start ignored (already running)`);
994
1287
  return;
995
1288
  }
996
1289
  if (this.agentsStarting.has(agentId)) {
997
- console.log(`[Agent ${agentId}] Start ignored (startup in progress)`);
1290
+ logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
998
1291
  return;
999
1292
  }
1000
1293
  this.agentsStarting.add(agentId);
1001
1294
  try {
1002
1295
  const driver = this.driverResolver(config.runtime || "claude");
1003
- const agentDataDir = path3.join(this.dataDir, agentId);
1296
+ const agentDataDir = path4.join(this.dataDir, agentId);
1004
1297
  await mkdir(agentDataDir, { recursive: true });
1005
- const memoryMdPath = path3.join(agentDataDir, "MEMORY.md");
1298
+ const memoryMdPath = path4.join(agentDataDir, "MEMORY.md");
1006
1299
  try {
1007
1300
  await access(memoryMdPath);
1008
1301
  } catch {
@@ -1020,7 +1313,7 @@ ${config.description || "No role defined yet."}
1020
1313
  `;
1021
1314
  await writeFile(memoryMdPath, initialMemoryMd);
1022
1315
  }
1023
- await mkdir(path3.join(agentDataDir, "notes"), { recursive: true });
1316
+ await mkdir(path4.join(agentDataDir, "notes"), { recursive: true });
1024
1317
  const isResume = !!config.sessionId;
1025
1318
  let prompt;
1026
1319
  if (isResume && resumePrompt) {
@@ -1030,14 +1323,11 @@ ${config.description || "No role defined yet."}
1030
1323
 
1031
1324
  Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
1032
1325
  }
1033
- } else if (isResume && wakeMessage) {
1034
- const channelLabel = wakeMessage.channel_type === "dm" ? `DM:@${wakeMessage.channel_name}` : `#${wakeMessage.channel_name}`;
1035
- const senderPrefix = wakeMessage.sender_type === "agent" ? "(agent) " : "";
1036
- const time = wakeMessage.timestamp ? ` (${toLocalTime(wakeMessage.timestamp)})` : "";
1037
- const formatted = `[${channelLabel}]${time} ${senderPrefix}@${wakeMessage.sender_name}: ${wakeMessage.content}`;
1326
+ } else if (wakeMessage) {
1327
+ const channelLabel = formatChannelLabel(wakeMessage);
1038
1328
  prompt = `New message received:
1039
1329
 
1040
- ${formatted}`;
1330
+ ${formatIncomingMessage(wakeMessage)}`;
1041
1331
  if (unreadSummary && Object.keys(unreadSummary).length > 0) {
1042
1332
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
1043
1333
  if (otherUnread.length > 0) {
@@ -1096,6 +1386,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1096
1386
  inbox: this.startingInboxes.get(agentId) || [],
1097
1387
  config,
1098
1388
  sessionId: config.sessionId || null,
1389
+ launchId: launchId || null,
1099
1390
  isIdle: false,
1100
1391
  notificationTimer: null,
1101
1392
  pendingNotificationCount: 0,
@@ -1107,7 +1398,8 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1107
1398
  lastRuntimeError: null,
1108
1399
  spawnError: null,
1109
1400
  exitCode: null,
1110
- exitSignal: null
1401
+ exitSignal: null,
1402
+ pendingTrajectory: null
1111
1403
  };
1112
1404
  this.startingInboxes.delete(agentId);
1113
1405
  this.agents.set(agentId, agentProcess);
@@ -1138,12 +1430,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1138
1430
  if (current) {
1139
1431
  current.recentStderr = pushRecentStderr(current.recentStderr, text);
1140
1432
  }
1141
- console.error(`[Agent ${agentId} stderr]: ${text}`);
1433
+ logger.error(`[Agent ${agentId} stderr]: ${text}`);
1142
1434
  });
1143
1435
  proc.on("error", (err) => {
1144
1436
  const current = this.agents.get(agentId);
1145
1437
  if (current) current.spawnError = err.message;
1146
- console.error(`[Agent ${agentId}] Process error: ${err.message}`);
1438
+ logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
1147
1439
  });
1148
1440
  proc.on("exit", (code, signal) => {
1149
1441
  const current = this.agents.get(agentId);
@@ -1151,7 +1443,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1151
1443
  current.exitCode = code;
1152
1444
  current.exitSignal = signal;
1153
1445
  }
1154
- console.log(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
1446
+ logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
1155
1447
  });
1156
1448
  proc.on("close", (code, signal) => {
1157
1449
  if (this.agents.has(agentId)) {
@@ -1160,6 +1452,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1160
1452
  if (ap.notificationTimer) {
1161
1453
  clearTimeout(ap.notificationTimer);
1162
1454
  }
1455
+ if (ap.pendingTrajectory?.timer) {
1456
+ clearTimeout(ap.pendingTrajectory.timer);
1457
+ }
1163
1458
  if (ap.activityHeartbeat) {
1164
1459
  clearInterval(ap.activityHeartbeat);
1165
1460
  }
@@ -1170,19 +1465,21 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1170
1465
  const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
1171
1466
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
1172
1467
  if (queuedWakeMessage) {
1173
- console.log(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
1468
+ logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
1174
1469
  const nextConfig = { ...ap.config, sessionId: ap.sessionId };
1175
1470
  this.idleAgentConfigs.set(agentId, {
1176
1471
  config: nextConfig,
1177
- sessionId: ap.sessionId
1472
+ sessionId: ap.sessionId,
1473
+ launchId: ap.launchId
1178
1474
  });
1179
1475
  this.broadcastActivity(agentId, "working", "Message received");
1180
1476
  this.idleAgentConfigs.delete(agentId);
1181
- this.startAgent(agentId, nextConfig, queuedWakeMessage, unreadSummary2).catch((err) => {
1182
- console.error(`[Agent ${agentId}] Failed to continue with queued message:`, err);
1477
+ this.startAgent(agentId, nextConfig, queuedWakeMessage, unreadSummary2, void 0, ap.launchId || void 0).catch((err) => {
1478
+ logger.error(`[Agent ${agentId}] Failed to continue with queued message`, err);
1183
1479
  this.idleAgentConfigs.set(agentId, {
1184
1480
  config: nextConfig,
1185
- sessionId: ap.sessionId
1481
+ sessionId: ap.sessionId,
1482
+ launchId: ap.launchId
1186
1483
  });
1187
1484
  this.broadcastActivity(agentId, "online", "Process idle");
1188
1485
  });
@@ -1190,10 +1487,11 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1190
1487
  }
1191
1488
  this.idleAgentConfigs.set(agentId, {
1192
1489
  config: { ...ap.config, sessionId: ap.sessionId },
1193
- sessionId: ap.sessionId
1490
+ sessionId: ap.sessionId,
1491
+ launchId: ap.launchId
1194
1492
  });
1195
1493
  if (!ap.driver.supportsStdinNotification) {
1196
- console.log(`[Agent ${agentId}] Turn completed; cached idle state for future restart`);
1494
+ logger.info(`[Agent ${agentId}] Turn completed; cached idle state for future restart`);
1197
1495
  }
1198
1496
  this.broadcastActivity(agentId, "online", "Process idle");
1199
1497
  } else {
@@ -1203,7 +1501,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1203
1501
  if (isMissingResumeSession(ap)) {
1204
1502
  const staleSessionId = ap.sessionId;
1205
1503
  const restartConfig = { ...ap.config, sessionId: null };
1206
- console.warn(
1504
+ logger.warn(
1207
1505
  `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
1208
1506
  );
1209
1507
  this.broadcastActivity(
@@ -1212,20 +1510,20 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1212
1510
  "Stored Claude session missing; cold-starting a new session\u2026",
1213
1511
  [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
1214
1512
  );
1215
- this.startAgent(agentId, restartConfig).catch((err) => {
1216
- console.error(`[Agent ${agentId}] Cold start recovery failed:`, err);
1217
- this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1513
+ this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
1514
+ logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
1515
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
1218
1516
  this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
1219
1517
  });
1220
1518
  return;
1221
1519
  }
1222
- console.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
1223
- this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1520
+ logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
1521
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
1224
1522
  this.broadcastActivity(agentId, "offline", `Crashed (${summary})`);
1225
1523
  }
1226
1524
  }
1227
1525
  });
1228
- this.sendToServer({ type: "agent:status", agentId, status: "active" });
1526
+ this.sendAgentStatus(agentId, "active", launchId || null);
1229
1527
  this.broadcastActivity(agentId, "working", "Starting\u2026");
1230
1528
  } catch (err) {
1231
1529
  this.agentsStarting.delete(agentId);
@@ -1237,7 +1535,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1237
1535
  const ap = this.agents.get(agentId);
1238
1536
  if (!ap) {
1239
1537
  if (!silent) {
1240
- console.log(`[Agent ${agentId}] Stop requested but no running process was found`);
1538
+ logger.info(`[Agent ${agentId}] Stop requested but no running process was found`);
1241
1539
  }
1242
1540
  return;
1243
1541
  }
@@ -1250,15 +1548,15 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1250
1548
  this.agents.delete(agentId);
1251
1549
  ap.process.kill("SIGTERM");
1252
1550
  if (!silent) {
1253
- this.sendToServer({ type: "agent:status", agentId, status: "inactive" });
1551
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
1254
1552
  this.broadcastActivity(agentId, "offline", "Stopped");
1255
- console.log(`[Agent ${agentId}] Stopped by request`);
1553
+ logger.info(`[Agent ${agentId}] Stopped by request`);
1256
1554
  }
1257
1555
  if (wait) {
1258
1556
  await new Promise((resolve) => {
1259
1557
  const forceKillTimer = setTimeout(() => {
1260
1558
  if (!silent) {
1261
- console.warn(`[Agent ${agentId}] Stop timed out; force killing`);
1559
+ logger.warn(`[Agent ${agentId}] Stop timed out; force killing`);
1262
1560
  }
1263
1561
  try {
1264
1562
  ap.process.kill("SIGKILL");
@@ -1288,10 +1586,10 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1288
1586
  }
1289
1587
  const cached = this.idleAgentConfigs.get(agentId);
1290
1588
  if (cached) {
1291
- console.log(`[Agent ${agentId}] Starting from idle state for new message`);
1589
+ logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
1292
1590
  this.idleAgentConfigs.delete(agentId);
1293
- this.startAgent(agentId, cached.config, message).catch((err) => {
1294
- console.error(`[Agent ${agentId}] Failed to auto-restart:`, err);
1591
+ this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
1592
+ logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
1295
1593
  });
1296
1594
  }
1297
1595
  return;
@@ -1299,7 +1597,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1299
1597
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
1300
1598
  ap.isIdle = false;
1301
1599
  this.broadcastActivity(agentId, "working", "Message received");
1302
- this.deliverMessageViaStdin(agentId, ap, message);
1600
+ this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
1303
1601
  return;
1304
1602
  }
1305
1603
  ap.inbox.push(message);
@@ -1313,12 +1611,12 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1313
1611
  }
1314
1612
  }
1315
1613
  async resetWorkspace(agentId) {
1316
- const agentDataDir = path3.join(this.dataDir, agentId);
1614
+ const agentDataDir = path4.join(this.dataDir, agentId);
1317
1615
  try {
1318
1616
  await rm(agentDataDir, { recursive: true, force: true });
1319
- console.log(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
1617
+ logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
1320
1618
  } catch (err) {
1321
- console.error(`[Agent ${agentId}] Workspace reset failed:`, err);
1619
+ logger.error(`[Agent ${agentId}] Workspace reset failed`, err);
1322
1620
  }
1323
1621
  }
1324
1622
  async stopAll() {
@@ -1332,10 +1630,13 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1332
1630
  getAgentSessionId(agentId) {
1333
1631
  return this.agents.get(agentId)?.sessionId ?? null;
1334
1632
  }
1633
+ getAgentLaunchId(agentId) {
1634
+ return this.agents.get(agentId)?.launchId ?? null;
1635
+ }
1335
1636
  getIdleAgentSessionIds() {
1336
1637
  const result = [];
1337
- for (const [agentId, { sessionId }] of this.idleAgentConfigs) {
1338
- if (sessionId) result.push({ agentId, sessionId });
1638
+ for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
1639
+ if (sessionId) result.push({ agentId, sessionId, launchId });
1339
1640
  }
1340
1641
  return result;
1341
1642
  }
@@ -1350,14 +1651,14 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1350
1651
  }
1351
1652
  for (const entry of entries) {
1352
1653
  if (!entry.isDirectory()) continue;
1353
- const dirPath = path3.join(this.dataDir, entry.name);
1654
+ const dirPath = path4.join(this.dataDir, entry.name);
1354
1655
  try {
1355
1656
  const dirContents = await readdir(dirPath, { withFileTypes: true });
1356
1657
  let totalSize = 0;
1357
1658
  let latestMtime = /* @__PURE__ */ new Date(0);
1358
1659
  let fileCount = 0;
1359
1660
  for (const item of dirContents) {
1360
- const itemPath = path3.join(dirPath, item.name);
1661
+ const itemPath = path4.join(dirPath, item.name);
1361
1662
  try {
1362
1663
  const info = await stat(itemPath);
1363
1664
  if (item.isFile()) {
@@ -1387,19 +1688,19 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1387
1688
  if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
1388
1689
  return false;
1389
1690
  }
1390
- const targetDir = path3.join(this.dataDir, directoryName);
1691
+ const targetDir = path4.join(this.dataDir, directoryName);
1391
1692
  try {
1392
1693
  await rm(targetDir, { recursive: true, force: true });
1393
- console.log(`[Workspace] Deleted directory: ${targetDir}`);
1694
+ logger.info(`[Workspace] Deleted directory: ${targetDir}`);
1394
1695
  return true;
1395
1696
  } catch (err) {
1396
- console.error(`[Workspace] Failed to delete directory ${targetDir}:`, err);
1697
+ logger.error(`[Workspace] Failed to delete directory ${targetDir}`, err);
1397
1698
  return false;
1398
1699
  }
1399
1700
  }
1400
1701
  // Workspace file browsing
1401
1702
  async getFileTree(agentId, dirPath) {
1402
- const agentDir = path3.join(this.dataDir, agentId);
1703
+ const agentDir = path4.join(this.dataDir, agentId);
1403
1704
  try {
1404
1705
  await stat(agentDir);
1405
1706
  } catch {
@@ -1407,8 +1708,8 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1407
1708
  }
1408
1709
  let targetDir = agentDir;
1409
1710
  if (dirPath) {
1410
- const resolved = path3.resolve(agentDir, dirPath);
1411
- if (!resolved.startsWith(agentDir + path3.sep) && resolved !== agentDir) {
1711
+ const resolved = path4.resolve(agentDir, dirPath);
1712
+ if (!resolved.startsWith(agentDir + path4.sep) && resolved !== agentDir) {
1412
1713
  return [];
1413
1714
  }
1414
1715
  targetDir = resolved;
@@ -1416,9 +1717,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1416
1717
  return this.listDirectoryChildren(targetDir, agentDir);
1417
1718
  }
1418
1719
  async readFile(agentId, filePath) {
1419
- const agentDir = path3.join(this.dataDir, agentId);
1420
- const resolved = path3.resolve(agentDir, filePath);
1421
- if (!resolved.startsWith(agentDir + path3.sep) && resolved !== agentDir) {
1720
+ const agentDir = path4.join(this.dataDir, agentId);
1721
+ const resolved = path4.resolve(agentDir, filePath);
1722
+ if (!resolved.startsWith(agentDir + path4.sep) && resolved !== agentDir) {
1422
1723
  throw new Error("Access denied");
1423
1724
  }
1424
1725
  const info = await stat(resolved);
@@ -1442,7 +1743,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1442
1743
  ".sh",
1443
1744
  ".py"
1444
1745
  ]);
1445
- const ext = path3.extname(resolved).toLowerCase();
1746
+ const ext = path4.extname(resolved).toLowerCase();
1446
1747
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
1447
1748
  return { content: null, binary: true };
1448
1749
  }
@@ -1469,13 +1770,13 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1469
1770
  const agent = this.agents.get(agentId);
1470
1771
  const runtime = runtimeHint || agent?.config.runtime || "claude";
1471
1772
  const home = os.homedir();
1472
- const workspaceDir = path3.join(this.dataDir, agentId);
1773
+ const workspaceDir = path4.join(this.dataDir, agentId);
1473
1774
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
1474
1775
  const globalResults = await Promise.all(
1475
- paths.global.map((p) => this.scanSkillsDir(path3.join(home, p)))
1776
+ paths.global.map((p) => this.scanSkillsDir(path4.join(home, p)))
1476
1777
  );
1477
1778
  const workspaceResults = await Promise.all(
1478
- paths.workspace.map((p) => this.scanSkillsDir(path3.join(workspaceDir, p)))
1779
+ paths.workspace.map((p) => this.scanSkillsDir(path4.join(workspaceDir, p)))
1479
1780
  );
1480
1781
  const dedup = (skills) => {
1481
1782
  const seen = /* @__PURE__ */ new Set();
@@ -1504,7 +1805,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1504
1805
  const skills = [];
1505
1806
  for (const entry of entries) {
1506
1807
  if (entry.isDirectory() || entry.isSymbolicLink()) {
1507
- const skillMd = path3.join(dir, entry.name, "SKILL.md");
1808
+ const skillMd = path4.join(dir, entry.name, "SKILL.md");
1508
1809
  try {
1509
1810
  const content = await readFile(skillMd, "utf-8");
1510
1811
  const skill = this.parseSkillMd(entry.name, content);
@@ -1515,7 +1816,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1515
1816
  } else if (entry.name.endsWith(".md")) {
1516
1817
  const cmdName = entry.name.replace(/\.md$/, "");
1517
1818
  try {
1518
- const content = await readFile(path3.join(dir, entry.name), "utf-8");
1819
+ const content = await readFile(path4.join(dir, entry.name), "utf-8");
1519
1820
  const skill = this.parseSkillMd(cmdName, content);
1520
1821
  skill.sourcePath = dir;
1521
1822
  skills.push(skill);
@@ -1558,14 +1859,20 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1558
1859
  if (!hasToolStart) {
1559
1860
  entries.push({ kind: "status", activity, detail });
1560
1861
  }
1561
- this.sendToServer({ type: "agent:activity", agentId, activity, detail, entries });
1862
+ this.sendToServer({ type: "agent:activity", agentId, activity, detail, entries, launchId: ap?.launchId || void 0 });
1562
1863
  if (ap) {
1563
1864
  ap.lastActivity = activity;
1564
1865
  ap.lastActivityDetail = detail;
1565
1866
  if (activity === "working" || activity === "thinking") {
1566
1867
  if (!ap.activityHeartbeat) {
1567
1868
  ap.activityHeartbeat = setInterval(() => {
1568
- this.sendToServer({ type: "agent:activity", agentId, activity: ap.lastActivity, detail: ap.lastActivityDetail });
1869
+ this.sendToServer({
1870
+ type: "agent:activity",
1871
+ agentId,
1872
+ activity: ap.lastActivity,
1873
+ detail: ap.lastActivityDetail,
1874
+ launchId: ap.launchId || void 0
1875
+ });
1569
1876
  }, ACTIVITY_HEARTBEAT_MS);
1570
1877
  }
1571
1878
  } else {
@@ -1576,28 +1883,64 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1576
1883
  }
1577
1884
  }
1578
1885
  }
1886
+ flushPendingTrajectory(agentId) {
1887
+ const ap = this.agents.get(agentId);
1888
+ const pending = ap?.pendingTrajectory;
1889
+ if (!ap || !pending) return;
1890
+ if (pending.timer) clearTimeout(pending.timer);
1891
+ ap.pendingTrajectory = null;
1892
+ const text = pending.text.length > MAX_TRAJECTORY_TEXT ? pending.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : pending.text;
1893
+ if (!text) return;
1894
+ if (pending.kind === "thinking") {
1895
+ this.broadcastActivity(agentId, "thinking", "", [{ kind: "thinking", text }]);
1896
+ } else {
1897
+ this.broadcastActivity(agentId, "thinking", "", [{ kind: "text", text }]);
1898
+ }
1899
+ }
1900
+ queueTrajectoryText(agentId, kind, text) {
1901
+ const ap = this.agents.get(agentId);
1902
+ if (!ap) return;
1903
+ if (!text) {
1904
+ this.broadcastActivity(agentId, "thinking", "");
1905
+ return;
1906
+ }
1907
+ const pending = ap.pendingTrajectory;
1908
+ if (pending && pending.kind === kind) {
1909
+ pending.text += text;
1910
+ if (pending.timer) clearTimeout(pending.timer);
1911
+ pending.timer = setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS);
1912
+ return;
1913
+ }
1914
+ this.flushPendingTrajectory(agentId);
1915
+ if (ap.lastActivity !== "thinking" || ap.lastActivityDetail !== "") {
1916
+ this.broadcastActivity(agentId, "thinking", "");
1917
+ }
1918
+ ap.pendingTrajectory = {
1919
+ kind,
1920
+ text,
1921
+ timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
1922
+ };
1923
+ }
1579
1924
  /** Handle a single ParsedEvent from any runtime driver */
1580
1925
  handleParsedEvent(agentId, event, driver) {
1581
1926
  const ap = this.agents.get(agentId);
1582
1927
  switch (event.kind) {
1583
1928
  case "session_init":
1584
1929
  if (ap) ap.sessionId = event.sessionId;
1585
- this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1930
+ this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
1586
1931
  break;
1587
1932
  case "thinking": {
1588
- const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
1589
- const extra = text ? [{ kind: "thinking", text }] : [];
1590
- this.broadcastActivity(agentId, "thinking", "", extra);
1933
+ this.queueTrajectoryText(agentId, "thinking", event.text);
1591
1934
  if (ap) ap.isIdle = false;
1592
1935
  break;
1593
1936
  }
1594
1937
  case "text": {
1595
- const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
1596
- this.broadcastActivity(agentId, "thinking", "", [{ kind: "text", text }]);
1938
+ this.queueTrajectoryText(agentId, "text", event.text);
1597
1939
  if (ap) ap.isIdle = false;
1598
1940
  break;
1599
1941
  }
1600
1942
  case "tool_call": {
1943
+ this.flushPendingTrajectory(agentId);
1601
1944
  const toolName = event.name;
1602
1945
  const inputSummary = driver.summarizeToolInput(toolName, event.input);
1603
1946
  const detail = toolName === `${driver.mcpToolPrefix}check_messages` ? "Checking messages\u2026" : toolName === `${driver.mcpToolPrefix}send_message` ? "Sending message\u2026" : driver.toolDisplayName(toolName);
@@ -1606,30 +1949,42 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1606
1949
  break;
1607
1950
  }
1608
1951
  case "turn_end":
1952
+ this.flushPendingTrajectory(agentId);
1609
1953
  if (ap) {
1610
1954
  if (event.sessionId) ap.sessionId = event.sessionId;
1611
1955
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
1612
- const nextMessage = ap.inbox.shift();
1956
+ const nextMessages = ap.inbox.splice(0, ap.inbox.length);
1613
1957
  this.broadcastActivity(agentId, "working", "Message received");
1614
- this.deliverMessageViaStdin(agentId, ap, nextMessage);
1958
+ this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
1615
1959
  } else {
1616
1960
  ap.isIdle = true;
1617
1961
  this.broadcastActivity(agentId, "online", "Idle");
1618
1962
  }
1619
1963
  }
1620
1964
  if (event.sessionId) {
1621
- this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1965
+ this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
1622
1966
  }
1623
1967
  break;
1624
1968
  case "error": {
1969
+ this.flushPendingTrajectory(agentId);
1625
1970
  if (ap) ap.lastRuntimeError = event.message;
1626
1971
  const currentActivity = ap?.lastActivity || "working";
1627
1972
  const currentDetail = ap?.lastActivityDetail || "";
1628
- this.sendToServer({ type: "agent:activity", agentId, activity: currentActivity, detail: currentDetail, entries: [{ kind: "text", text: `Error: ${event.message}` }] });
1973
+ this.sendToServer({
1974
+ type: "agent:activity",
1975
+ agentId,
1976
+ activity: currentActivity,
1977
+ detail: currentDetail,
1978
+ entries: [{ kind: "text", text: `Error: ${event.message}` }],
1979
+ launchId: ap?.launchId || void 0
1980
+ });
1629
1981
  break;
1630
1982
  }
1631
1983
  }
1632
1984
  }
1985
+ sendAgentStatus(agentId, status, launchId) {
1986
+ this.sendToServer({ type: "agent:status", agentId, status, launchId: launchId || void 0 });
1987
+ }
1633
1988
  /** Send a batched notification to the agent via stdin about pending messages */
1634
1989
  sendStdinNotification(agentId) {
1635
1990
  const ap = this.agents.get(agentId);
@@ -1640,27 +1995,38 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1640
1995
  if (count === 0) return;
1641
1996
  if (ap.isIdle) return;
1642
1997
  if (!ap.sessionId) return;
1998
+ if (ap.driver.deliverMessageDirectlyWhileBusy && ap.inbox.length > 0) {
1999
+ const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
2000
+ console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
2001
+ this.broadcastActivity(agentId, "working", "Message received");
2002
+ this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy");
2003
+ return;
2004
+ }
1643
2005
  const notification = `[System notification: You have ${count} new message${count > 1 ? "s" : ""} waiting. Call check_messages to read ${count > 1 ? "them" : "it"} when you're ready.]`;
1644
- console.log(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
1645
- const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId);
2006
+ logger.info(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
2007
+ const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
1646
2008
  if (encoded) {
1647
2009
  ap.process.stdin?.write(encoded + "\n");
1648
2010
  }
1649
2011
  }
1650
2012
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
1651
- deliverMessageViaStdin(agentId, ap, message) {
1652
- const channelLabel = message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
1653
- const senderPrefix = message.sender_type === "agent" ? "(agent) " : "";
1654
- const time = message.timestamp ? ` (${toLocalTime(message.timestamp)})` : "";
1655
- const formatted = `[${channelLabel}]${time} ${senderPrefix}@${message.sender_name}: ${message.content}`;
1656
- const prompt = `New message received:
2013
+ deliverMessagesViaStdin(agentId, ap, messages, mode) {
2014
+ if (messages.length === 0) return;
2015
+ const prompt = messages.length === 1 ? `New message received:
1657
2016
 
1658
- ${formatted}
2017
+ ${formatIncomingMessage(messages[0])}
2018
+
2019
+ Respond as appropriate. Complete all your work before stopping.` : `New messages received:
2020
+
2021
+ ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
1659
2022
 
1660
2023
  Respond as appropriate. Complete all your work before stopping.`;
1661
- const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId);
2024
+ const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
1662
2025
  if (encoded) {
1663
- console.log(`[Agent ${agentId}] Delivering message via stdin from @${message.sender_name}`);
2026
+ const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
2027
+ logger.info(
2028
+ `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
2029
+ );
1664
2030
  ap.process.stdin?.write(encoded + "\n");
1665
2031
  }
1666
2032
  }
@@ -1680,8 +2046,8 @@ Respond as appropriate. Complete all your work before stopping.`;
1680
2046
  const nodes = [];
1681
2047
  for (const entry of entries) {
1682
2048
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1683
- const fullPath = path3.join(dir, entry.name);
1684
- const relativePath = path3.relative(rootDir, fullPath);
2049
+ const fullPath = path4.join(dir, entry.name);
2050
+ const relativePath = path4.relative(rootDir, fullPath);
1685
2051
  let info;
1686
2052
  try {
1687
2053
  info = await stat(fullPath);
@@ -1747,7 +2113,7 @@ var RUNTIMES = [
1747
2113
  { id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
1748
2114
  { id: "codex", displayName: "Codex CLI", binary: "codex", supported: true },
1749
2115
  { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: false },
1750
- { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: false }
2116
+ { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true }
1751
2117
  ];
1752
2118
 
1753
2119
  // src/index.ts
@@ -1801,12 +2167,12 @@ if (!serverUrl || !apiKey) {
1801
2167
  console.error("Usage: slock-daemon --server-url <url> --api-key <key>");
1802
2168
  process.exit(1);
1803
2169
  }
1804
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
1805
- var chatBridgePath = path4.resolve(__dirname, "chat-bridge.js");
2170
+ var __dirname = path5.dirname(fileURLToPath(import.meta.url));
2171
+ var chatBridgePath = path5.resolve(__dirname, "chat-bridge.js");
1806
2172
  try {
1807
2173
  accessSync(chatBridgePath);
1808
2174
  } catch {
1809
- chatBridgePath = path4.resolve(__dirname, "chat-bridge.ts");
2175
+ chatBridgePath = path5.resolve(__dirname, "chat-bridge.ts");
1810
2176
  }
1811
2177
  var connection;
1812
2178
  var agentManager = new AgentProcessManager(chatBridgePath, (msg) => {
@@ -1817,27 +2183,27 @@ connection = new DaemonConnection({
1817
2183
  apiKey,
1818
2184
  onMessage: (msg) => {
1819
2185
  const summary = summarizeIncomingMessage(msg);
1820
- console.log(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
2186
+ logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
1821
2187
  switch (msg.type) {
1822
2188
  case "agent:start":
1823
- console.log(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
1824
- agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt).catch((err) => {
2189
+ logger.info(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
2190
+ agentManager.startAgent(msg.agentId, msg.config, msg.wakeMessage, msg.unreadSummary, msg.resumePrompt, msg.launchId).catch((err) => {
1825
2191
  const reason = err instanceof Error ? err.message : String(err);
1826
- console.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
1827
- connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive" });
1828
- connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}` });
2192
+ logger.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
2193
+ connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive", launchId: msg.launchId });
2194
+ connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}`, launchId: msg.launchId });
1829
2195
  });
1830
2196
  break;
1831
2197
  case "agent:stop":
1832
- console.log(`[Agent ${msg.agentId}] Stop requested`);
2198
+ logger.info(`[Agent ${msg.agentId}] Stop requested`);
1833
2199
  agentManager.stopAgent(msg.agentId);
1834
2200
  break;
1835
2201
  case "agent:reset-workspace":
1836
- console.log(`[Agent ${msg.agentId}] Workspace reset requested`);
2202
+ logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
1837
2203
  agentManager.resetWorkspace(msg.agentId);
1838
2204
  break;
1839
2205
  case "agent:deliver":
1840
- console.log(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
2206
+ logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
1841
2207
  agentManager.deliverMessage(msg.agentId, msg.message);
1842
2208
  connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
1843
2209
  break;
@@ -1869,18 +2235,18 @@ connection = new DaemonConnection({
1869
2235
  agentManager.listSkills(msg.agentId, msg.runtime).then(({ global, workspace }) => {
1870
2236
  connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global, workspace });
1871
2237
  }).catch((err) => {
1872
- console.error(`[Daemon] Failed to list skills for ${msg.agentId}:`, err);
2238
+ logger.error(`[Daemon] Failed to list skills for ${msg.agentId}`, err);
1873
2239
  connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
1874
2240
  });
1875
2241
  break;
1876
2242
  case "machine:workspace:scan":
1877
- console.log("[Daemon] Scanning all workspace directories");
2243
+ logger.info("[Daemon] Scanning all workspace directories");
1878
2244
  agentManager.scanAllWorkspaces().then((directories) => {
1879
2245
  connection.send({ type: "machine:workspace:scan_result", directories });
1880
2246
  });
1881
2247
  break;
1882
2248
  case "machine:workspace:delete":
1883
- console.log(`[Daemon] Deleting workspace directory: ${msg.directoryName}`);
2249
+ logger.info(`[Daemon] Deleting workspace directory: ${msg.directoryName}`);
1884
2250
  agentManager.deleteWorkspaceDirectory(msg.directoryName).then((success) => {
1885
2251
  connection.send({ type: "machine:workspace:delete_result", directoryName: msg.directoryName, success });
1886
2252
  });
@@ -1892,7 +2258,7 @@ connection = new DaemonConnection({
1892
2258
  },
1893
2259
  onConnect: () => {
1894
2260
  const runtimes = detectRuntimes();
1895
- console.log(`[Daemon] Detected runtimes: ${runtimes.join(", ") || "none"}`);
2261
+ logger.info(`[Daemon] Detected runtimes: ${runtimes.join(", ") || "none"}`);
1896
2262
  connection.send({
1897
2263
  type: "ready",
1898
2264
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
@@ -1904,22 +2270,23 @@ connection = new DaemonConnection({
1904
2270
  });
1905
2271
  for (const agentId of agentManager.getRunningAgentIds()) {
1906
2272
  const sessionId = agentManager.getAgentSessionId(agentId);
2273
+ const launchId = agentManager.getAgentLaunchId(agentId);
1907
2274
  if (sessionId) {
1908
- connection.send({ type: "agent:session", agentId, sessionId });
2275
+ connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
1909
2276
  }
1910
2277
  }
1911
- for (const { agentId, sessionId } of agentManager.getIdleAgentSessionIds()) {
1912
- connection.send({ type: "agent:session", agentId, sessionId });
2278
+ for (const { agentId, sessionId, launchId } of agentManager.getIdleAgentSessionIds()) {
2279
+ connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
1913
2280
  }
1914
2281
  },
1915
2282
  onDisconnect: () => {
1916
- console.log("[Daemon] Lost connection \u2014 agents continue running locally");
2283
+ logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
1917
2284
  }
1918
2285
  });
1919
- console.log("[Slock Daemon] Starting...");
2286
+ logger.info("[Slock Daemon] Starting...");
1920
2287
  connection.connect();
1921
2288
  var shutdown = async () => {
1922
- console.log("[Slock Daemon] Shutting down...");
2289
+ logger.info("[Slock Daemon] Shutting down...");
1923
2290
  await agentManager.stopAll();
1924
2291
  connection.disconnect();
1925
2292
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.30.0",
3
+ "version": "0.30.1-alpha.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"
@@ -17,12 +17,12 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@modelcontextprotocol/sdk": "^1.26.0",
21
- "ws": "^8.19.0",
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
21
+ "ws": "^8.20.0",
22
22
  "zod": "^4.3.6"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/node": "^25.3.0",
25
+ "@types/node": "^25.5.0",
26
26
  "@types/ws": "^8.18.1",
27
27
  "tsup": "^8.5.1",
28
28
  "typescript": "^5.9.3",