@slock-ai/daemon 0.24.0 → 0.26.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.
@@ -256,62 +256,6 @@ server.tool(
256
256
  }
257
257
  }
258
258
  );
259
- server.tool(
260
- "wait_for_message",
261
- "Block and wait for new messages. ONLY call this when you have NO work left to do \u2014 all tasks complete, all replies sent, nothing pending. This blocks for up to ~4 minutes waiting for a message to arrive. If you still have work in progress, do NOT call this \u2014 finish your work first.",
262
- {},
263
- async () => {
264
- try {
265
- const POLL_INTERVAL_MS = 25e3;
266
- const MAX_WAIT_MS = 27e4;
267
- const startTime = Date.now();
268
- while (true) {
269
- const elapsed = Date.now() - startTime;
270
- const remaining = MAX_WAIT_MS - elapsed;
271
- if (remaining <= 0) {
272
- const params2 = new URLSearchParams();
273
- params2.set("block", "true");
274
- params2.set("timeout", "1000");
275
- const res2 = await fetch(
276
- `${serverUrl}/internal/agent/${agentId}/receive?${params2}`,
277
- { method: "GET", headers: commonHeaders }
278
- );
279
- if (!res2.ok) {
280
- const errData = await res2.json().catch(() => ({}));
281
- return { content: [{ type: "text", text: `Error: ${errData.error || res2.statusText}` }] };
282
- }
283
- const data2 = await res2.json();
284
- if (data2.messages?.length > 0) {
285
- return { content: [{ type: "text", text: formatMessages(data2.messages) }] };
286
- }
287
- return {
288
- content: [{ type: "text", text: "No new messages. Take a rest \u2014 new messages will be delivered to you directly. Do not take any further action until notified." }]
289
- };
290
- }
291
- const thisPoll = Math.min(POLL_INTERVAL_MS, remaining);
292
- const params = new URLSearchParams();
293
- params.set("block", "true");
294
- params.set("timeout", String(thisPoll));
295
- const res = await fetch(
296
- `${serverUrl}/internal/agent/${agentId}/receive?${params}`,
297
- { method: "GET", headers: commonHeaders }
298
- );
299
- if (!res.ok) {
300
- const errData = await res.json().catch(() => ({}));
301
- return { content: [{ type: "text", text: `Error: ${errData.error || res.statusText}` }] };
302
- }
303
- const data = await res.json();
304
- if (data.messages?.length > 0) {
305
- return { content: [{ type: "text", text: formatMessages(data.messages) }] };
306
- }
307
- }
308
- } catch (err) {
309
- return {
310
- content: [{ type: "text", text: `Error: ${err.message}` }]
311
- };
312
- }
313
- }
314
- );
315
259
  function formatMessages(messages) {
316
260
  return messages.map((m) => {
317
261
  const target = formatTarget(m);
package/dist/index.js CHANGED
@@ -98,17 +98,19 @@ function toolRef(prefix, name) {
98
98
  }
99
99
  function buildBaseSystemPrompt(config, opts) {
100
100
  const t = (name) => toolRef(opts.toolPrefix, name);
101
+ const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
101
102
  const criticalRules = [
102
103
  `- Do NOT output text directly. ALL communication goes through ${t("send_message")}.`,
103
104
  ...opts.extraCriticalRules,
104
- `- Do NOT explore the filesystem looking for messaging scripts. The MCP tools are already available.`
105
+ `- Do NOT explore the filesystem looking for messaging scripts. The MCP tools are already available.`,
106
+ `- NEVER start working on a task without claiming it first via ${t("claim_tasks")}. If the claim fails, do NOT work on it.`
105
107
  ];
106
108
  const startupSteps = [
107
- `1. **Read MEMORY.md** (in your cwd). This is your memory index \u2014 it tells you what you know and where to find it.`,
108
- `2. Follow the instructions in MEMORY.md to read any other memory files you need (e.g. channel summaries, role definitions, user preferences).`,
109
- `3. Call ${t("wait_for_message")}() to start listening.`,
109
+ `1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with ${t("send_message")} before deep context gathering.`,
110
+ `2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.`,
111
+ `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
110
112
  `4. When you receive a message, process it and reply with ${t("send_message")}.`,
111
- `5. **Only when you have NO more work to do**, call ${t("wait_for_message")}() to wait for new messages. NEVER call wait_for_message while you still have work in progress \u2014 finish all steps first, report your results, then go back to listening.`
113
+ `5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. New messages arrive automatically \u2014 you do not need to poll or wait for them.`
112
114
  ];
113
115
  let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
114
116
 
@@ -120,18 +122,17 @@ You are a **long-running, persistent agent**. You are NOT a one-shot assistant \
120
122
 
121
123
  You have MCP tools from the "chat" server. Use ONLY these for communication:
122
124
 
123
- 1. **${t("wait_for_message")}** \u2014 Block and wait for new messages. ONLY use when you have NO work left to do. This is your idle loop.
124
- 2. **${t("check_messages")}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
125
- 3. **${t("send_message")}** \u2014 Send a message to a channel or DM.
126
- 4. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
127
- 5. **${t("read_history")}** \u2014 Read past messages from a channel or DM.
128
- 6. **${t("list_tasks")}** \u2014 View a channel's task board.
129
- 7. **${t("create_tasks")}** \u2014 Create tasks on a channel's task board (supports batch).
130
- 8. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
131
- 9. **${t("unclaim_task")}** \u2014 Release your claim on a task.
132
- 10. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
133
- 11. **${t("upload_file")}** \u2014 Upload an image file to attach to a message. Returns an attachment ID to pass to send_message.
134
- 12. **${t("view_file")}** \u2014 Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
125
+ 1. **${t("check_messages")}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
126
+ 2. **${t("send_message")}** \u2014 Send a message to a channel or DM.
127
+ 3. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
128
+ 4. **${t("read_history")}** \u2014 Read past messages from a channel or DM.
129
+ 5. **${t("list_tasks")}** \u2014 View a channel's task board.
130
+ 6. **${t("create_tasks")}** \u2014 Create tasks on a channel's task board (supports batch).
131
+ 7. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
132
+ 8. **${t("unclaim_task")}** \u2014 Release your claim on a task.
133
+ 9. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
134
+ 10. **${t("upload_file")}** \u2014 Upload an image file to attach to a message. Returns an attachment ID to pass to send_message.
135
+ 11. **${t("view_file")}** \u2014 Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
135
136
 
136
137
  CRITICAL RULES:
137
138
  ${criticalRules.join("\n")}
@@ -218,7 +219,7 @@ Each channel has a task board with two independent dimensions: **status** (progr
218
219
  - **Unclaim**: \`unclaim_task(channel="#channel-name", task_number=3)\` \u2014 remove your assignment. Does not change progress status.
219
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.
220
221
 
221
- **CRITICAL: You MUST claim a task before starting work on it.** Never begin working on a task without claiming it first. The claim mechanism prevents multiple agents from doing the same work. If your claim fails (someone else claimed it), move on to another task.
222
+ **CRITICAL: You MUST claim a task before starting ANY work on it.** Call \`${t("claim_tasks")}\` first. If the claim fails (someone else already claimed it), you MUST NOT work on that task \u2014 move on to another one. This is the only way to prevent duplicate work across agents. No exceptions.
222
223
 
223
224
  **IMPORTANT: When you finish a task, use \`update_task_status(..., status="in_review")\`.** This gives humans a chance to validate your work before it's marked as done. Only set status to \`done\` directly for trivial tasks that don't need review.
224
225
 
@@ -251,7 +252,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
251
252
 
252
253
  - **Don't interrupt ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person \u2014 not at you. Do NOT jump in unless you are explicitly @mentioned or clearly addressed.
253
254
  - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work \u2014 let them respond to questions about it.
254
- - **Claim before you start.** When picking up a task, announce it in the channel first to avoid duplicate work by others.
255
+ - **Claim before you start.** Always call \`${t("claim_tasks")}\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
255
256
 
256
257
  ### Formatting \u2014 No HTML
257
258
 
@@ -340,7 +341,7 @@ While you are busy (executing tools, thinking, etc.), new messages may arrive. W
340
341
  How to handle these:
341
342
  - Call \`${t("check_messages")}()\` to check for new messages. You are encouraged to do this frequently \u2014 at natural breakpoints in your work, or whenever you see a notification.
342
343
  - If the new message is higher priority, you may pivot to it. If not, continue your current work.
343
- - **IMPORTANT: \`${t("check_messages")}\` and \`${t("wait_for_message")}\` are very different tools.** \`check_messages\` returns instantly with any pending messages (or "no new messages"). \`wait_for_message\` blocks for up to ~4 minutes \u2014 ONLY use it when you have absolutely NO work left to do.`;
344
+ - \`check_messages\` returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
344
345
  }
345
346
  if (config.description) {
346
347
  prompt += `
@@ -718,11 +719,10 @@ var CodexDriver = class {
718
719
  return buildBaseSystemPrompt(config, {
719
720
  toolPrefix: "",
720
721
  extraCriticalRules: [
721
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything.",
722
- "- ALWAYS call wait_for_message after completing any task \u2014 this keeps you listening for new messages."
722
+ "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
723
723
  ],
724
724
  postStartupNotes: [
725
- "**IMPORTANT**: Your process exits after each turn completes. You will be automatically restarted when new messages arrive. Always call wait_for_message as your last action \u2014 if no messages are pending, you'll sleep and be woken when one arrives."
725
+ "**IMPORTANT**: Your process exits after each turn completes. You will be automatically restarted when new messages arrive. Complete all your work, then stop \u2014 new messages will wake you up."
726
726
  ],
727
727
  includeStdinNotificationSection: false
728
728
  });
@@ -803,8 +803,23 @@ function toLocalTime(iso) {
803
803
  const pad = (n) => String(n).padStart(2, "0");
804
804
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
805
805
  }
806
+ function formatChannelLabel(message) {
807
+ return message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
808
+ }
809
+ function buildUnreadSummary(messages, excludeChannel) {
810
+ const summary = /* @__PURE__ */ new Map();
811
+ for (const message of messages) {
812
+ const label = formatChannelLabel(message);
813
+ if (excludeChannel && label === excludeChannel) continue;
814
+ summary.set(label, (summary.get(label) || 0) + 1);
815
+ }
816
+ return summary.size > 0 ? Object.fromEntries(summary) : void 0;
817
+ }
806
818
  var MAX_TRAJECTORY_TEXT = 2e3;
807
819
  var ACTIVITY_HEARTBEAT_MS = 6e4;
820
+ function getMessageDeliveryText(supportsStdinNotification) {
821
+ return supportsStdinNotification ? "New messages will be delivered to you automatically via stdin." : "The daemon will automatically restart you when new messages arrive.";
822
+ }
808
823
  var AgentProcessManager = class {
809
824
  agents = /* @__PURE__ */ new Map();
810
825
  agentsStarting = /* @__PURE__ */ new Set();
@@ -814,17 +829,21 @@ var AgentProcessManager = class {
814
829
  chatBridgePath;
815
830
  sendToServer;
816
831
  daemonApiKey;
817
- constructor(chatBridgePath2, sendToServer, daemonApiKey) {
832
+ dataDir;
833
+ driverResolver;
834
+ constructor(chatBridgePath2, sendToServer, daemonApiKey, opts = {}) {
818
835
  this.chatBridgePath = chatBridgePath2;
819
836
  this.sendToServer = sendToServer;
820
837
  this.daemonApiKey = daemonApiKey;
838
+ this.dataDir = opts.dataDir || DATA_DIR;
839
+ this.driverResolver = opts.driverResolver || getDriver;
821
840
  }
822
841
  async startAgent(agentId, config, wakeMessage, unreadSummary) {
823
842
  if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
824
843
  this.agentsStarting.add(agentId);
825
844
  try {
826
- const driver = getDriver(config.runtime || "claude");
827
- const agentDataDir = path3.join(DATA_DIR, agentId);
845
+ const driver = this.driverResolver(config.runtime || "claude");
846
+ const agentDataDir = path3.join(this.dataDir, agentId);
828
847
  await mkdir(agentDataDir, { recursive: true });
829
848
  const memoryMdPath = path3.join(agentDataDir, "MEMORY.md");
830
849
  try {
@@ -872,9 +891,9 @@ Use read_history to catch up, or respond to the message above first.`;
872
891
  }
873
892
  prompt += `
874
893
 
875
- Respond as appropriate \u2014 reply using send_message, or take action as needed. Then call wait_for_message to keep listening.
894
+ Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
876
895
 
877
- IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before calling wait_for_message. Sending a progress update does NOT mean your task is done \u2014 only call wait_for_message when you have NO more work to do.`;
896
+ IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
878
897
  if (driver.supportsStdinNotification) {
879
898
  prompt += `
880
899
 
@@ -888,14 +907,14 @@ Note: While you are busy, you may receive [System notification: ...] messages. F
888
907
  }
889
908
  prompt += `
890
909
 
891
- Use read_history to catch up on important channels, then call wait_for_message to listen for new messages.`;
910
+ Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: reply with send_message and claim the relevant task before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
892
911
  if (driver.supportsStdinNotification) {
893
912
  prompt += `
894
913
 
895
914
  Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
896
915
  }
897
916
  } else if (isResume) {
898
- prompt = `No new messages while you were away. Call ${driver.mcpToolPrefix}wait_for_message to listen for new messages.`;
917
+ prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
899
918
  if (driver.supportsStdinNotification) {
900
919
  prompt += `
901
920
 
@@ -916,10 +935,9 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
916
935
  process: proc,
917
936
  driver,
918
937
  inbox: [],
919
- pendingReceive: null,
920
938
  config,
921
939
  sessionId: config.sessionId || null,
922
- isInReceiveMessage: false,
940
+ isIdle: false,
923
941
  notificationTimer: null,
924
942
  pendingNotificationCount: 0,
925
943
  activityHeartbeat: null,
@@ -952,10 +970,6 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
952
970
  if (this.agents.has(agentId)) {
953
971
  const ap = this.agents.get(agentId);
954
972
  if (ap.process !== proc) return;
955
- if (ap.pendingReceive) {
956
- clearTimeout(ap.pendingReceive.timer);
957
- ap.pendingReceive.resolve([]);
958
- }
959
973
  if (ap.notificationTimer) {
960
974
  clearTimeout(ap.notificationTimer);
961
975
  }
@@ -964,6 +978,26 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
964
978
  }
965
979
  this.agents.delete(agentId);
966
980
  if (code === 0) {
981
+ const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
982
+ const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
983
+ if (queuedWakeMessage) {
984
+ const nextConfig = { ...ap.config, sessionId: ap.sessionId };
985
+ this.idleAgentConfigs.set(agentId, {
986
+ config: nextConfig,
987
+ sessionId: ap.sessionId
988
+ });
989
+ this.broadcastActivity(agentId, "working", "Message received");
990
+ this.idleAgentConfigs.delete(agentId);
991
+ this.startAgent(agentId, nextConfig, queuedWakeMessage, unreadSummary2).catch((err) => {
992
+ console.error(`[Agent ${agentId}] Failed to continue with queued message:`, err);
993
+ this.idleAgentConfigs.set(agentId, {
994
+ config: nextConfig,
995
+ sessionId: ap.sessionId
996
+ });
997
+ this.broadcastActivity(agentId, "online", "Process idle");
998
+ });
999
+ return;
1000
+ }
967
1001
  this.idleAgentConfigs.set(agentId, {
968
1002
  config: { ...ap.config, sessionId: ap.sessionId },
969
1003
  sessionId: ap.sessionId
@@ -989,10 +1023,6 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
989
1023
  this.idleAgentConfigs.delete(agentId);
990
1024
  const ap = this.agents.get(agentId);
991
1025
  if (!ap) return;
992
- if (ap.pendingReceive) {
993
- clearTimeout(ap.pendingReceive.timer);
994
- ap.pendingReceive.resolve([]);
995
- }
996
1026
  if (ap.notificationTimer) {
997
1027
  clearTimeout(ap.notificationTimer);
998
1028
  }
@@ -1038,19 +1068,14 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1038
1068
  }
1039
1069
  return;
1040
1070
  }
1041
- if (ap.isInReceiveMessage) {
1042
- ap.isInReceiveMessage = false;
1071
+ if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
1072
+ ap.isIdle = false;
1043
1073
  this.broadcastActivity(agentId, "working", "Message received");
1074
+ this.deliverMessageViaStdin(agentId, ap, message);
1075
+ return;
1044
1076
  }
1045
- if (ap.pendingReceive) {
1046
- clearTimeout(ap.pendingReceive.timer);
1047
- ap.pendingReceive.resolve([message]);
1048
- ap.pendingReceive = null;
1049
- } else {
1050
- ap.inbox.push(message);
1051
- }
1077
+ ap.inbox.push(message);
1052
1078
  if (!ap.driver.supportsStdinNotification) return;
1053
- if (ap.isInReceiveMessage) return;
1054
1079
  if (!ap.sessionId) return;
1055
1080
  ap.pendingNotificationCount++;
1056
1081
  if (!ap.notificationTimer) {
@@ -1060,7 +1085,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1060
1085
  }
1061
1086
  }
1062
1087
  async resetWorkspace(agentId) {
1063
- const agentDataDir = path3.join(DATA_DIR, agentId);
1088
+ const agentDataDir = path3.join(this.dataDir, agentId);
1064
1089
  try {
1065
1090
  await rm(agentDataDir, { recursive: true, force: true });
1066
1091
  console.log(`[Agent ${agentId}] Workspace deleted: ${agentDataDir}`);
@@ -1081,7 +1106,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1081
1106
  const results = [];
1082
1107
  let entries;
1083
1108
  try {
1084
- entries = await readdir(DATA_DIR, { withFileTypes: true });
1109
+ entries = await readdir(this.dataDir, { withFileTypes: true });
1085
1110
  } catch {
1086
1111
  return [];
1087
1112
  }
@@ -1124,7 +1149,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1124
1149
  if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
1125
1150
  return false;
1126
1151
  }
1127
- const targetDir = path3.join(DATA_DIR, directoryName);
1152
+ const targetDir = path3.join(this.dataDir, directoryName);
1128
1153
  try {
1129
1154
  await rm(targetDir, { recursive: true, force: true });
1130
1155
  console.log(`[Workspace] Deleted directory: ${targetDir}`);
@@ -1136,7 +1161,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1136
1161
  }
1137
1162
  // Workspace file browsing
1138
1163
  async getFileTree(agentId, dirPath) {
1139
- const agentDir = path3.join(DATA_DIR, agentId);
1164
+ const agentDir = path3.join(this.dataDir, agentId);
1140
1165
  try {
1141
1166
  await stat(agentDir);
1142
1167
  } catch {
@@ -1153,7 +1178,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1153
1178
  return this.listDirectoryChildren(targetDir, agentDir);
1154
1179
  }
1155
1180
  async readFile(agentId, filePath) {
1156
- const agentDir = path3.join(DATA_DIR, agentId);
1181
+ const agentDir = path3.join(this.dataDir, agentId);
1157
1182
  const resolved = path3.resolve(agentDir, filePath);
1158
1183
  if (!resolved.startsWith(agentDir + path3.sep) && resolved !== agentDir) {
1159
1184
  throw new Error("Access denied");
@@ -1229,40 +1254,34 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1229
1254
  const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
1230
1255
  const extra = text ? [{ kind: "thinking", text }] : [];
1231
1256
  this.broadcastActivity(agentId, "thinking", "", extra);
1232
- if (ap) ap.isInReceiveMessage = false;
1257
+ if (ap) ap.isIdle = false;
1233
1258
  break;
1234
1259
  }
1235
1260
  case "text": {
1236
1261
  const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
1237
1262
  this.broadcastActivity(agentId, "thinking", "", [{ kind: "text", text }]);
1238
- if (ap) ap.isInReceiveMessage = false;
1263
+ if (ap) ap.isIdle = false;
1239
1264
  break;
1240
1265
  }
1241
1266
  case "tool_call": {
1242
1267
  const toolName = event.name;
1243
1268
  const inputSummary = driver.summarizeToolInput(toolName, event.input);
1244
- if (toolName === `${driver.mcpToolPrefix}wait_for_message`) {
1245
- this.broadcastActivity(agentId, "online", "Idle");
1246
- if (ap) {
1247
- ap.isInReceiveMessage = true;
1248
- ap.pendingNotificationCount = 0;
1249
- if (ap.notificationTimer) {
1250
- clearTimeout(ap.notificationTimer);
1251
- ap.notificationTimer = null;
1252
- }
1253
- }
1254
- } else {
1255
- const detail = toolName === `${driver.mcpToolPrefix}check_messages` ? "Checking messages\u2026" : toolName === `${driver.mcpToolPrefix}send_message` ? "Sending message\u2026" : driver.toolDisplayName(toolName);
1256
- this.broadcastActivity(agentId, "working", detail, [{ kind: "tool_start", toolName, toolInput: inputSummary }]);
1257
- if (ap) ap.isInReceiveMessage = false;
1258
- }
1269
+ const detail = toolName === `${driver.mcpToolPrefix}check_messages` ? "Checking messages\u2026" : toolName === `${driver.mcpToolPrefix}send_message` ? "Sending message\u2026" : driver.toolDisplayName(toolName);
1270
+ this.broadcastActivity(agentId, "working", detail, [{ kind: "tool_start", toolName, toolInput: inputSummary }]);
1271
+ if (ap) ap.isIdle = false;
1259
1272
  break;
1260
1273
  }
1261
1274
  case "turn_end":
1262
- this.broadcastActivity(agentId, "online", "Turn complete");
1263
1275
  if (ap) {
1264
- ap.isInReceiveMessage = false;
1265
1276
  if (event.sessionId) ap.sessionId = event.sessionId;
1277
+ if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
1278
+ const nextMessage = ap.inbox.shift();
1279
+ this.broadcastActivity(agentId, "working", "Message received");
1280
+ this.deliverMessageViaStdin(agentId, ap, nextMessage);
1281
+ } else {
1282
+ ap.isIdle = true;
1283
+ this.broadcastActivity(agentId, "online", "Idle");
1284
+ }
1266
1285
  }
1267
1286
  if (event.sessionId) {
1268
1287
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
@@ -1284,7 +1303,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1284
1303
  ap.pendingNotificationCount = 0;
1285
1304
  ap.notificationTimer = null;
1286
1305
  if (count === 0) return;
1287
- if (ap.isInReceiveMessage) return;
1306
+ if (ap.isIdle) return;
1288
1307
  if (!ap.sessionId) return;
1289
1308
  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.]`;
1290
1309
  console.log(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
@@ -1293,6 +1312,23 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1293
1312
  ap.process.stdin?.write(encoded + "\n");
1294
1313
  }
1295
1314
  }
1315
+ /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
1316
+ deliverMessageViaStdin(agentId, ap, message) {
1317
+ const channelLabel = message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
1318
+ const senderPrefix = message.sender_type === "agent" ? "(agent) " : "";
1319
+ const time = message.timestamp ? ` (${toLocalTime(message.timestamp)})` : "";
1320
+ const formatted = `[${channelLabel}]${time} ${senderPrefix}@${message.sender_name}: ${message.content}`;
1321
+ const prompt = `New message received:
1322
+
1323
+ ${formatted}
1324
+
1325
+ Respond as appropriate. Complete all your work before stopping.`;
1326
+ const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId);
1327
+ if (encoded) {
1328
+ console.log(`[Agent ${agentId}] Delivering message via stdin from @${message.sender_name}`);
1329
+ ap.process.stdin?.write(encoded + "\n");
1330
+ }
1331
+ }
1296
1332
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
1297
1333
  async listDirectoryChildren(dir, rootDir) {
1298
1334
  let entries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"
@@ -32,6 +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
36
  "typecheck": "tsc --noEmit",
36
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",
37
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",