@slock-ai/daemon 0.25.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.
Files changed (2) hide show
  1. package/dist/index.js +58 -17
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -98,15 +98,17 @@ 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. Stop and wait. New messages will be delivered to you automatically via stdin.`,
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
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
  ];
@@ -217,7 +219,7 @@ Each channel has a task board with two independent dimensions: **status** (progr
217
219
  - **Unclaim**: \`unclaim_task(channel="#channel-name", task_number=3)\` \u2014 remove your assignment. Does not change progress status.
218
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.
219
221
 
220
- **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.
221
223
 
222
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.
223
225
 
@@ -250,7 +252,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
250
252
 
251
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.
252
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.
253
- - **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.
254
256
 
255
257
  ### Formatting \u2014 No HTML
256
258
 
@@ -801,8 +803,23 @@ function toLocalTime(iso) {
801
803
  const pad = (n) => String(n).padStart(2, "0");
802
804
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
803
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
+ }
804
818
  var MAX_TRAJECTORY_TEXT = 2e3;
805
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
+ }
806
823
  var AgentProcessManager = class {
807
824
  agents = /* @__PURE__ */ new Map();
808
825
  agentsStarting = /* @__PURE__ */ new Set();
@@ -812,17 +829,21 @@ var AgentProcessManager = class {
812
829
  chatBridgePath;
813
830
  sendToServer;
814
831
  daemonApiKey;
815
- constructor(chatBridgePath2, sendToServer, daemonApiKey) {
832
+ dataDir;
833
+ driverResolver;
834
+ constructor(chatBridgePath2, sendToServer, daemonApiKey, opts = {}) {
816
835
  this.chatBridgePath = chatBridgePath2;
817
836
  this.sendToServer = sendToServer;
818
837
  this.daemonApiKey = daemonApiKey;
838
+ this.dataDir = opts.dataDir || DATA_DIR;
839
+ this.driverResolver = opts.driverResolver || getDriver;
819
840
  }
820
841
  async startAgent(agentId, config, wakeMessage, unreadSummary) {
821
842
  if (this.agents.has(agentId) || this.agentsStarting.has(agentId)) return;
822
843
  this.agentsStarting.add(agentId);
823
844
  try {
824
- const driver = getDriver(config.runtime || "claude");
825
- const agentDataDir = path3.join(DATA_DIR, agentId);
845
+ const driver = this.driverResolver(config.runtime || "claude");
846
+ const agentDataDir = path3.join(this.dataDir, agentId);
826
847
  await mkdir(agentDataDir, { recursive: true });
827
848
  const memoryMdPath = path3.join(agentDataDir, "MEMORY.md");
828
849
  try {
@@ -872,7 +893,7 @@ Use read_history to catch up, or respond to the message above first.`;
872
893
 
873
894
  Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
874
895
 
875
- 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. New messages will be delivered to you automatically via stdin.`;
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)}`;
876
897
  if (driver.supportsStdinNotification) {
877
898
  prompt += `
878
899
 
@@ -886,14 +907,14 @@ Note: While you are busy, you may receive [System notification: ...] messages. F
886
907
  }
887
908
  prompt += `
888
909
 
889
- Use read_history to catch up on important channels, then stop. New messages will be delivered to you automatically.`;
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)}`;
890
911
  if (driver.supportsStdinNotification) {
891
912
  prompt += `
892
913
 
893
914
  Note: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.`;
894
915
  }
895
916
  } else if (isResume) {
896
- prompt = `No new messages while you were away. Nothing to do \u2014 just stop. New messages will be delivered to you automatically via stdin.`;
917
+ prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver.supportsStdinNotification)}`;
897
918
  if (driver.supportsStdinNotification) {
898
919
  prompt += `
899
920
 
@@ -957,6 +978,26 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
957
978
  }
958
979
  this.agents.delete(agentId);
959
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
+ }
960
1001
  this.idleAgentConfigs.set(agentId, {
961
1002
  config: { ...ap.config, sessionId: ap.sessionId },
962
1003
  sessionId: ap.sessionId
@@ -1044,7 +1085,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1044
1085
  }
1045
1086
  }
1046
1087
  async resetWorkspace(agentId) {
1047
- const agentDataDir = path3.join(DATA_DIR, agentId);
1088
+ const agentDataDir = path3.join(this.dataDir, agentId);
1048
1089
  try {
1049
1090
  await rm(agentDataDir, { recursive: true, force: true });
1050
1091
  console.log(`[Agent ${agentId}] Workspace deleted: ${agentDataDir}`);
@@ -1065,7 +1106,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1065
1106
  const results = [];
1066
1107
  let entries;
1067
1108
  try {
1068
- entries = await readdir(DATA_DIR, { withFileTypes: true });
1109
+ entries = await readdir(this.dataDir, { withFileTypes: true });
1069
1110
  } catch {
1070
1111
  return [];
1071
1112
  }
@@ -1108,7 +1149,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1108
1149
  if (directoryName.includes("/") || directoryName.includes("..") || directoryName.includes("\\")) {
1109
1150
  return false;
1110
1151
  }
1111
- const targetDir = path3.join(DATA_DIR, directoryName);
1152
+ const targetDir = path3.join(this.dataDir, directoryName);
1112
1153
  try {
1113
1154
  await rm(targetDir, { recursive: true, force: true });
1114
1155
  console.log(`[Workspace] Deleted directory: ${targetDir}`);
@@ -1120,7 +1161,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1120
1161
  }
1121
1162
  // Workspace file browsing
1122
1163
  async getFileTree(agentId, dirPath) {
1123
- const agentDir = path3.join(DATA_DIR, agentId);
1164
+ const agentDir = path3.join(this.dataDir, agentId);
1124
1165
  try {
1125
1166
  await stat(agentDir);
1126
1167
  } catch {
@@ -1137,7 +1178,7 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1137
1178
  return this.listDirectoryChildren(targetDir, agentDir);
1138
1179
  }
1139
1180
  async readFile(agentId, filePath) {
1140
- const agentDir = path3.join(DATA_DIR, agentId);
1181
+ const agentDir = path3.join(this.dataDir, agentId);
1141
1182
  const resolved = path3.resolve(agentDir, filePath);
1142
1183
  if (!resolved.startsWith(agentDir + path3.sep) && resolved !== agentDir) {
1143
1184
  throw new Error("Access denied");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.25.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",