@slock-ai/daemon 0.39.0 → 0.39.1-alpha.1

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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-E6OOH3IC.js";
5
5
 
6
6
  // src/core.ts
7
- import path10 from "path";
7
+ import path11 from "path";
8
8
  import os4 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
@@ -37,6 +37,9 @@ var TOOL_DISPLAY_METADATA = {
37
37
  web_fetch: { logLabel: "Fetching web", activityLabel: "Fetching web\u2026", summaryKind: "url" },
38
38
  web_search: { logLabel: "Searching web", activityLabel: "Searching web\u2026", summaryKind: "query" },
39
39
  todo_write: { logLabel: "Updating tasks", activityLabel: "Updating tasks\u2026", summaryKind: "none" },
40
+ schedule_reminder: { logLabel: "Scheduling reminder", activityLabel: "Scheduling reminder\u2026", summaryKind: "reminder_title" },
41
+ list_reminders: { logLabel: "Listing reminders", activityLabel: "Listing reminders\u2026", summaryKind: "none" },
42
+ cancel_reminder: { logLabel: "Canceling reminder", activityLabel: "Canceling reminder\u2026", summaryKind: "reminder_id" },
40
43
  collab_tool_call: { logLabel: "Collaborating", activityLabel: "Collaborating\u2026", summaryKind: "none" }
41
44
  };
42
45
  var KNOWN_TOOL_ALIASES = {
@@ -89,6 +92,9 @@ var KNOWN_TOOL_ALIASES = {
89
92
  SearchWeb: "web_search",
90
93
  TodoWrite: "todo_write",
91
94
  SetTodoList: "todo_write",
95
+ schedule_reminder: "schedule_reminder",
96
+ list_reminders: "list_reminders",
97
+ cancel_reminder: "cancel_reminder",
92
98
  collab_tool_call: "collab_tool_call"
93
99
  };
94
100
  var MCP_CHAT_NAMESPACE_PREFIXES = ["mcp__chat__", "mcp_chat_"];
@@ -119,6 +125,198 @@ function resolveToolSemantic(toolName) {
119
125
  const normalized = normalizeToolLookupName(toolName);
120
126
  return KNOWN_TOOL_ALIASES[normalized] ?? null;
121
127
  }
128
+ function tokenizeShellCommand(command) {
129
+ const tokens = [];
130
+ let current = "";
131
+ let quote = null;
132
+ let escaping = false;
133
+ for (const ch of command) {
134
+ if (escaping) {
135
+ current += ch;
136
+ escaping = false;
137
+ continue;
138
+ }
139
+ if (quote === "'") {
140
+ if (ch === "'") {
141
+ quote = null;
142
+ } else {
143
+ current += ch;
144
+ }
145
+ continue;
146
+ }
147
+ if (quote === '"') {
148
+ if (ch === '"') {
149
+ quote = null;
150
+ } else if (ch === "\\") {
151
+ escaping = true;
152
+ } else {
153
+ current += ch;
154
+ }
155
+ continue;
156
+ }
157
+ if (ch === "'" || ch === '"') {
158
+ quote = ch;
159
+ continue;
160
+ }
161
+ if (ch === "\\") {
162
+ escaping = true;
163
+ continue;
164
+ }
165
+ if (/\s/.test(ch)) {
166
+ if (current) {
167
+ tokens.push(current);
168
+ current = "";
169
+ }
170
+ continue;
171
+ }
172
+ current += ch;
173
+ }
174
+ if (escaping || quote) return null;
175
+ if (current) tokens.push(current);
176
+ return tokens;
177
+ }
178
+ function isEnvAssignmentToken(token) {
179
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
180
+ }
181
+ function isSlockExecutableToken(token) {
182
+ const lastSep = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
183
+ const base = (lastSep >= 0 ? token.slice(lastSep + 1) : token).toLowerCase();
184
+ return base === "slock" || base === "slock.cmd";
185
+ }
186
+ function isShellExecutableToken(token) {
187
+ const lastSep = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
188
+ const base = (lastSep >= 0 ? token.slice(lastSep + 1) : token).toLowerCase();
189
+ return base === "bash" || base === "zsh" || base === "sh";
190
+ }
191
+ function findSlockExecutableIndex(tokens) {
192
+ const commandStartIndexes = [0];
193
+ for (let i = 0; i < tokens.length; i += 1) {
194
+ if (tokens[i] === "|" || tokens[i] === "&&" || tokens[i] === "||" || tokens[i] === ";") {
195
+ commandStartIndexes.push(i + 1);
196
+ }
197
+ }
198
+ for (const start of commandStartIndexes) {
199
+ let executableIndex = start;
200
+ while (executableIndex < tokens.length && isEnvAssignmentToken(tokens[executableIndex])) {
201
+ executableIndex += 1;
202
+ }
203
+ if (executableIndex < tokens.length && isSlockExecutableToken(tokens[executableIndex])) {
204
+ return executableIndex;
205
+ }
206
+ }
207
+ return -1;
208
+ }
209
+ function unwrapShellPayload(tokens, executableIndex) {
210
+ if (!isShellExecutableToken(tokens[executableIndex])) return null;
211
+ for (let i = executableIndex + 1; i < tokens.length; i++) {
212
+ const arg = tokens[i];
213
+ if (arg.startsWith("-") && arg.endsWith("c")) {
214
+ return i + 1 < tokens.length ? tokens[i + 1] : null;
215
+ }
216
+ if (!arg.startsWith("-")) break;
217
+ }
218
+ return null;
219
+ }
220
+ function readOptionValues(args, flag) {
221
+ const values = [];
222
+ for (let i = 0; i < args.length; i += 1) {
223
+ const arg = args[i];
224
+ if (arg === flag && i + 1 < args.length) {
225
+ values.push(args[i + 1]);
226
+ i += 1;
227
+ continue;
228
+ }
229
+ if (arg.startsWith(`${flag}=`)) {
230
+ values.push(arg.slice(flag.length + 1));
231
+ }
232
+ }
233
+ return values;
234
+ }
235
+ function readOptionValue(args, flag) {
236
+ return readOptionValues(args, flag).at(-1);
237
+ }
238
+ function parsePositiveIntegers(args, flag) {
239
+ return readOptionValues(args, flag).map((value) => Number(value)).filter((value) => Number.isFinite(value) && Number.isInteger(value) && value > 0);
240
+ }
241
+ function resolveSlockCliInvocation(toolName, input) {
242
+ if (resolveToolSemantic(toolName) !== "bash") return null;
243
+ const value = asObject(input);
244
+ if (!value || typeof value.command !== "string") return null;
245
+ const tokens = tokenizeShellCommand(value.command);
246
+ if (!tokens || tokens.length === 0) return null;
247
+ const firstExecutableIndex = (() => {
248
+ let index = 0;
249
+ while (index < tokens.length && isEnvAssignmentToken(tokens[index])) index += 1;
250
+ return index;
251
+ })();
252
+ if (firstExecutableIndex >= tokens.length) return null;
253
+ if (isShellExecutableToken(tokens[firstExecutableIndex])) {
254
+ const innerCommand = unwrapShellPayload(tokens, firstExecutableIndex);
255
+ if (innerCommand) {
256
+ return resolveSlockCliInvocation(toolName, { command: innerCommand });
257
+ }
258
+ }
259
+ const executableIndex = findSlockExecutableIndex(tokens);
260
+ if (executableIndex < 0) return null;
261
+ const cliArgs = tokens.slice(executableIndex + 1);
262
+ const [resource, action, ...rest] = cliArgs;
263
+ if (!resource || !action) return null;
264
+ switch (`${resource} ${action}`) {
265
+ case "message send":
266
+ return { toolName: "send_message", input: { target: readOptionValue(rest, "--target") } };
267
+ case "message check":
268
+ return { toolName: "check_messages", input: {} };
269
+ case "message read":
270
+ return { toolName: "read_history", input: { channel: readOptionValue(rest, "--channel") } };
271
+ case "message search":
272
+ return { toolName: "search_messages", input: { query: readOptionValue(rest, "--query") } };
273
+ case "server info":
274
+ return { toolName: "list_server", input: {} };
275
+ case "task list":
276
+ return { toolName: "list_tasks", input: { channel: readOptionValue(rest, "--channel") } };
277
+ case "task create":
278
+ return { toolName: "create_tasks", input: { channel: readOptionValue(rest, "--channel") } };
279
+ case "task claim":
280
+ return {
281
+ toolName: "claim_tasks",
282
+ input: {
283
+ channel: readOptionValue(rest, "--channel"),
284
+ task_numbers: parsePositiveIntegers(rest, "--number")
285
+ }
286
+ };
287
+ case "task unclaim":
288
+ return {
289
+ toolName: "unclaim_task",
290
+ input: {
291
+ channel: readOptionValue(rest, "--channel"),
292
+ task_number: parsePositiveIntegers(rest, "--number")[0]
293
+ }
294
+ };
295
+ case "task update":
296
+ return {
297
+ toolName: "update_task_status",
298
+ input: {
299
+ channel: readOptionValue(rest, "--channel"),
300
+ task_number: parsePositiveIntegers(rest, "--number")[0]
301
+ }
302
+ };
303
+ case "attachment upload":
304
+ return { toolName: "upload_file", input: { path: readOptionValue(rest, "--path") } };
305
+ case "attachment view":
306
+ return { toolName: "view_file", input: {} };
307
+ case "reminder schedule":
308
+ return { toolName: "schedule_reminder", input: { title: readOptionValue(rest, "--title") } };
309
+ case "reminder list":
310
+ return { toolName: "list_reminders", input: {} };
311
+ case "reminder cancel":
312
+ return { toolName: "cancel_reminder", input: { reminder_id: readOptionValue(rest, "--id") } };
313
+ default:
314
+ return null;
315
+ }
316
+ }
317
+ function normalizeToolDisplayInvocation(toolName, input) {
318
+ return resolveSlockCliInvocation(toolName, input) ?? { toolName, input };
319
+ }
122
320
  function getToolActivityLabel(toolName) {
123
321
  const semantic = resolveToolSemantic(toolName);
124
322
  if (semantic) return TOOL_DISPLAY_METADATA[semantic].activityLabel;
@@ -157,6 +355,14 @@ function summarizeToolInput(toolName, input) {
157
355
  return value.channel && value.task_number != null ? `${value.channel} #t${value.task_number}` : "";
158
356
  case "target":
159
357
  return value.target || "";
358
+ case "reminder_title": {
359
+ const title = value.title;
360
+ return typeof title === "string" ? truncateLabel(title, 40) : "";
361
+ }
362
+ case "reminder_id": {
363
+ const id = value.reminder_id;
364
+ return typeof id === "string" ? `#${id.slice(0, 8)}` : "";
365
+ }
160
366
  }
161
367
  }
162
368
 
@@ -270,129 +476,208 @@ var DISPLAY_PLAN_CONFIG = {
270
476
 
271
477
  // src/agentProcessManager.ts
272
478
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
273
- import path9 from "path";
479
+ import path10 from "path";
274
480
  import os3 from "os";
275
481
 
276
482
  // src/drivers/claude.ts
277
483
  import { spawn } from "child_process";
278
- import { writeFileSync } from "fs";
279
- import path2 from "path";
484
+ import path3 from "path";
485
+
486
+ // src/drivers/cliTransport.ts
487
+ import { mkdirSync, writeFileSync } from "fs";
488
+ import path from "path";
280
489
 
281
490
  // src/drivers/systemPrompt.ts
282
491
  function toolRef(prefix, name) {
283
492
  return `${prefix}${name}`;
284
493
  }
285
- function buildBaseSystemPrompt(config, opts) {
494
+ function buildPrompt(config, variant, opts) {
495
+ const isCli = variant === "cli";
286
496
  const t = (name) => toolRef(opts.toolPrefix, name);
497
+ const sendCmd = isCli ? "`slock message send`" : `\`${t("send_message")}\``;
498
+ const readCmd = isCli ? "`slock message read`" : `\`${t("read_history")}\``;
499
+ const checkCmd = isCli ? "`slock message check`" : `\`${t("check_messages")}\``;
500
+ const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
501
+ const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
502
+ const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
503
+ const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
287
504
  const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
288
- const criticalRules = [
289
- `- Always communicate through ${t("send_message")}. This is your only output channel.`,
505
+ const criticalRules = isCli ? [
506
+ "- Always communicate through `slock` CLI commands. This is your only output channel.",
290
507
  ...opts.extraCriticalRules,
291
- `- Use only the provided MCP tools for messaging \u2014 they are already available and ready.`,
292
- `- Always claim a task via ${t("claim_tasks")} before starting work on it. If the claim fails, move on to a different task.`
508
+ "- Use only the provided `slock` CLI commands for messaging.",
509
+ "- Always claim a task via `slock task claim` before starting work on it. If the claim fails, move on to a different task."
510
+ ] : [
511
+ `- Always communicate through ${sendCmd}. This is your only output channel.`,
512
+ ...opts.extraCriticalRules,
513
+ "- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
514
+ `- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
293
515
  ];
294
- const startupSteps = [
295
- `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.`,
296
- `2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.`,
516
+ const startupSteps = isCli ? [
517
+ "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 `slock message send` before deep context gathering.",
518
+ "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
519
+ `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
520
+ "4. When you receive a message, process it and reply with `slock message send`.",
521
+ "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."
522
+ ] : [
523
+ `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 ${sendCmd} before deep context gathering.`,
524
+ "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
297
525
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
298
- `4. When you receive a message, process it and reply with ${t("send_message")}.`,
299
- `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.`
526
+ `4. When you receive a message, process it and reply with ${sendCmd}.`,
527
+ "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."
300
528
  ];
301
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
302
-
303
- ## Who you are
529
+ const communicationSection = isCli ? `## Communication \u2014 slock CLI ONLY
530
+
531
+ Use the \`slock\` CLI for chat / task / attachment operations. The daemon injects a local \`slock\` wrapper into PATH for you. Use ONLY these commands for communication:
532
+
533
+ 1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
534
+ 2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
535
+ 3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
536
+ 4. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
537
+ 5. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
538
+ 6. **\`slock task list\`** \u2014 View a channel's task board.
539
+ 7. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
540
+ 8. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
541
+ 9. **\`slock task unclaim\`** \u2014 Release your claim on a task.
542
+ 10. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
543
+ 11. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
544
+ 12. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
545
+ 13. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
546
+ 14. **\`slock reminder list\`** \u2014 List your reminders.
547
+ 15. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
548
+
549
+ When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
550
+
551
+ The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
552
+ - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
553
+
554
+ Error code prefixes tell you the layer:
555
+ - \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
556
+ - \`*_FAILED\` = 4xx from server
557
+ - \`SERVER_5XX\` = server unreachable / crashed` : `## Communication \u2014 MCP tools ONLY
304
558
 
305
- Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
559
+ You have MCP tools from the "chat" server. Use ONLY these for communication:
306
560
 
307
- ## Communication \u2014 MCP tools ONLY
561
+ 1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
562
+ 2. **${sendCmd}** \u2014 Send a message to a channel or DM.
563
+ 3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
564
+ 4. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
565
+ 5. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
566
+ 6. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
567
+ 7. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
568
+ 8. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
569
+ 9. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
570
+ 10. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
571
+ 11. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
572
+ 12. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
573
+ const sendingMessagesSection = isCli ? `### Sending messages
574
+
575
+ - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
576
+ - **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
577
+ - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
578
+ - **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
579
+
580
+ Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
581
+ \`\`\`bash
582
+ slock message send --target "#channel-name" <<'EOF'
583
+ Long message with "quotes", $vars, \`backticks\`, and code blocks.
584
+ EOF
585
+ \`\`\`
308
586
 
309
- You have MCP tools from the "chat" server. Use ONLY these for communication:
587
+ **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.` : `### Sending messages
310
588
 
311
- 1. **${t("check_messages")}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
312
- 2. **${t("send_message")}** \u2014 Send a message to a channel or DM.
313
- 3. **${t("list_server")}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
314
- 4. **${t("read_history")}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
315
- 5. **${t("search_messages")}** \u2014 Search messages visible to you, then inspect a hit with \`${t("read_history")}\`.
316
- 6. **${t("list_tasks")}** \u2014 View a channel's task board.
317
- 7. **${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).
318
- 8. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
319
- 9. **${t("unclaim_task")}** \u2014 Release your claim on a task.
320
- 10. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
321
- 11. **${t("upload_file")}** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to send_message.
322
- 12. **${t("view_file")}** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
589
+ - **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
590
+ - **Reply to a DM**: \`${t("send_message")}(target="dm:@peer-name", content="...")\`
591
+ - **Reply in a thread**: \`${t("send_message")}(target="#channel:shortid", content="...")\` or \`${t("send_message")}(target="dm:@peer:shortid", content="...")\`
592
+ - **Start a NEW DM**: \`${t("send_message")}(target="dm:@person-name", content="...")\`
323
593
 
324
- CRITICAL RULES:
325
- ${criticalRules.join("\n")}
594
+ **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.`;
595
+ const threadsSection = isCli ? `### Threads
326
596
 
327
- ## Startup sequence
597
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
328
598
 
329
- ${startupSteps.join("\n")}`;
330
- if (opts.postStartupNotes.length > 0) {
331
- prompt += `
599
+ - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
600
+ - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
601
+ - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`slock message send --target "#general:a1b2c3d4" <<'EOF'\` followed by the message body and \`EOF\`. The thread will be auto-created if it doesn't exist yet.
602
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
603
+ - You can read thread history: \`slock message read --channel "#general:a1b2c3d4"\`
604
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
332
605
 
333
- ${opts.postStartupNotes.join("\n")}`;
334
- }
335
- prompt += `
606
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
336
607
 
337
- ## Messaging
608
+ - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
609
+ - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
610
+ - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, call \`${t("send_message")}(target="#general:a1b2c3d4", content="...")\`. The thread will be auto-created if it doesn't exist yet.
611
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
612
+ - You can read thread history via ${readCmd} with the same thread target.
613
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
614
+ const discoverySection = isCli ? `### Discovering people and channels
338
615
 
339
- Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
616
+ Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
617
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.` : `### Discovering people and channels
340
618
 
341
- \`\`\`
342
- [target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
343
- [target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
344
- [target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
345
- [target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03 type=human] @richard: thread reply
346
- [target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
347
- \`\`\`
619
+ Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
620
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.`;
621
+ const channelAwarenessSection = isCli ? `### Channel awareness
348
622
 
349
- Header fields:
350
- - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
351
- - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
352
- - \`time=\` \u2014 timestamp.
353
- - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
623
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
624
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
625
+ - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
626
+ - If unsure where something belongs, call \`slock server info\` to review channel descriptions.` : `### Channel awareness
354
627
 
355
- \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
628
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via ${serverInfoCmd}). Respect them:
629
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
630
+ - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
631
+ - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
632
+ const readingHistorySection = isCli ? `### Reading history
356
633
 
357
- ### Sending messages
634
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
358
635
 
359
- - **Reply to a channel**: \`send_message(target="#channel-name", content="...")\`
360
- - **Reply to a DM**: \`send_message(target="dm:@peer-name", content="...")\`
361
- - **Reply in a thread**: \`send_message(target="#channel:shortid", content="...")\` or \`send_message(target="dm:@peer:shortid", content="...")\`
362
- - **Start a NEW DM**: \`send_message(target="dm:@person-name", content="...")\`
636
+ To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
363
637
 
364
- **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.
638
+ Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
365
639
 
366
- ### Threads
640
+ To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
641
+ const tasksSection = isCli ? `### Tasks
367
642
 
368
- Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
643
+ When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
369
644
 
370
- - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
371
- - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
372
- - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`send_message(target="#general:a1b2c3d4", content="...")\`. The thread will be auto-created if it doesn't exist yet.
373
- - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
374
- - You can read thread history: \`read_history(channel="#general:a1b2c3d4")\`
375
- - Threads cannot be nested \u2014 you cannot start a thread inside a thread.
645
+ **Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
376
646
 
377
- ### Discovering people and channels
647
+ **What you see in messages:**
648
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
649
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
650
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
378
651
 
379
- Call \`list_server\` to see all channels in this server, which ones you have joined, other agents, and humans.
380
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`read_history\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.
652
+ Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context \u2014 reply there, but keep claims and conversions to top-level messages.
381
653
 
382
- ### Channel awareness
654
+ \`slock message read\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
383
655
 
384
- Each channel has a **name** and optionally a **description** that define its purpose (visible via \`list_server\`). Respect them:
385
- - **Reply in context** \u2014 always respond in the channel/thread the message came from.
386
- - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
387
- - If unsure where something belongs, call \`list_server\` to review channel descriptions.
656
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
388
657
 
389
- ### Reading history
658
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
390
659
 
391
- \`read_history(channel="#channel-name")\` or \`read_history(channel="dm:@peer-name")\` or \`read_history(channel="#channel:shortid")\`
660
+ **Workflow:**
661
+ 1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
662
+ 2. If the claim fails, someone else is working on it \u2014 move on to another task
663
+ 3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'EOF'\` followed by the message body and \`EOF\`
664
+ 4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
665
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
392
666
 
393
- To jump directly to a specific hit with nearby context, use \`read_history(channel="...", around="messageId")\` or \`read_history(channel="...", around=12345)\`.
667
+ **What \`slock task create\` really means:**
668
+ - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
669
+ - \`slock task create\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
670
+ - \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
671
+ - Typical uses for \`slock task create\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
672
+ - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
673
+ - If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
394
674
 
395
- ### Tasks
675
+ **Creating new tasks:**
676
+ - 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.
677
+ - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
678
+ - Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
679
+ - Reuse existing tasks and threads instead of creating duplicates.
680
+ - Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.` : `### Tasks
396
681
 
397
682
  When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
398
683
 
@@ -405,7 +690,7 @@ When someone sends a message that asks you to do something \u2014 fix a bug, wri
405
690
 
406
691
  Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context \u2014 reply there, but keep claims and conversions to top-level messages.
407
692
 
408
- \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
693
+ ${readCmd} shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
409
694
 
410
695
  **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
411
696
 
@@ -414,24 +699,77 @@ Only top-level channel / DM messages can become tasks. Messages inside threads a
414
699
  **Workflow:**
415
700
  1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
416
701
  2. If the claim fails, someone else is working on it \u2014 move on to another task
417
- 3. Post updates in the task's thread: \`send_message(target="#channel:msgShortId", ...)\`
418
- 4. When done, set status to \`in_review\` so a human can validate
702
+ 3. Post updates in the task's thread via ${sendCmd} with \`target="#channel:msgShortId"\`
703
+ 4. When done, set status to \`in_review\` so a human can validate via ${taskUpdateCmd}
419
704
  5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
420
705
 
421
- **What \`${t("create_tasks")}\` really means:**
706
+ **What ${taskCreateCmd} really means:**
422
707
  - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
423
- - \`${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.
424
- - \`${t("create_tasks")}\` only creates the task \u2014 to own it, call \`${t("claim_tasks")}\` afterward.
425
- - 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.
708
+ - ${taskCreateCmd} is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
709
+ - ${taskCreateCmd} only creates the task \u2014 to own it, call ${taskClaimCmd} afterward.
710
+ - Typical uses for ${taskCreateCmd} are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
426
711
  - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
427
- - If the work already exists as a message, reuse it via \`${t("claim_tasks")}\` with \`message_ids\`.
712
+ - If the work already exists as a message, reuse it via ${taskClaimCmd} with the message ID.
428
713
 
429
714
  **Creating new tasks:**
430
715
  - 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.
431
716
  - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
432
- - Before calling \`${t("create_tasks")}\`, first check whether the work already exists on the task board or is already being handled.
717
+ - Before calling ${taskCreateCmd}, first check whether the work already exists on the task board or is already being handled.
433
718
  - Reuse existing tasks and threads instead of creating duplicates.
434
- - Use \`${t("create_tasks")}\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.
719
+ - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
720
+ const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
721
+ let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
722
+
723
+ ## Who you are
724
+
725
+ Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
726
+
727
+ ${communicationSection}
728
+
729
+ CRITICAL RULES:
730
+ ${criticalRules.join("\n")}
731
+
732
+ ## Startup sequence
733
+
734
+ ${startupSteps.join("\n")}`;
735
+ if (opts.postStartupNotes.length > 0) {
736
+ prompt += `
737
+
738
+ ${opts.postStartupNotes.join("\n")}`;
739
+ }
740
+ prompt += `
741
+
742
+ ## Messaging
743
+
744
+ Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
745
+
746
+ \`\`\`
747
+ [target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
748
+ [target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
749
+ [target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
750
+ [target=#general:a1b2c3d4 msg=f3a4b5c6 time=2026-03-15T01:00:03 type=human] @richard: thread reply
751
+ [target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
752
+ \`\`\`
753
+
754
+ Header fields:
755
+ - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
756
+ - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
757
+ - \`time=\` \u2014 timestamp.
758
+ - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
759
+
760
+ \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
761
+
762
+ ${sendingMessagesSection}
763
+
764
+ ${threadsSection}
765
+
766
+ ${discoverySection}
767
+
768
+ ${channelAwarenessSection}
769
+
770
+ ${readingHistorySection}
771
+
772
+ ${tasksSection}
435
773
 
436
774
  ### Splitting tasks for parallel execution
437
775
 
@@ -445,8 +783,8 @@ When you receive a notification about new tasks, check the task board and claim
445
783
  ## @Mentions
446
784
 
447
785
  In channel group chats, you can @mention people by their unique name (e.g. @alice or @bob).
448
- - Your stable Slock @mention handle is @${config.name}.
449
- - Your display name is ${config.displayName || config.name}. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable name.
786
+ - Your stable Slock @mention handle is \`@${config.name}\`.
787
+ - Your display name is \`${config.displayName || config.name}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
450
788
  - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
451
789
  - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
452
790
  - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.
@@ -463,7 +801,7 @@ Keep the user informed. They cannot see your internal reasoning, so:
463
801
 
464
802
  - **Respect 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 only join if you are explicitly @mentioned or clearly addressed.
465
803
  - **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.
466
- - **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.
804
+ - **Claim before you start.** Always call ${claimForEtiquette} before doing any work on a task. If the claim fails, stop immediately and pick a different task.
467
805
  - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or channel before stopping.
468
806
  - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.
469
807
 
@@ -559,20 +897,21 @@ How to handle these:
559
897
  - Treat direct follow-up messages as new user input for the same live session.
560
898
  - Adapt if the new message changes priority or direction.
561
899
  - You do NOT need to poll just because direct follow-up delivery is available.
562
- - Use \`${t("check_messages")}()\` only when you need to inspect other pending channels or recover broader context.`;
900
+ - Use ${checkCmd} only when you need to inspect other pending channels or recover broader context.`;
563
901
  } else {
902
+ const notifyExample = isCli ? `\`[System notification: You have N new message(s) waiting. Call slock message check to read them when you're ready.]\`` : `\`[System notification: You have N new message(s) waiting. Call ${t("check_messages")} to read them when you're ready.]\``;
564
903
  prompt += `
565
904
 
566
905
  ## Message Notifications
567
906
 
568
907
  While you are busy (executing tools, thinking, etc.), new messages may arrive. When this happens, you will receive a system notification like:
569
908
 
570
- \`[System notification: You have N new message(s) waiting. Call check_messages to read them when you're ready.]\`
909
+ ${notifyExample}
571
910
 
572
911
  How to handle these:
573
- - 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.
912
+ - Call ${checkCmd} 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.
574
913
  - If the new message is higher priority, you may pivot to it. If not, continue your current work.
575
- - \`check_messages\` returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
914
+ - ${checkCmd} returns instantly with any pending messages (or "no new messages"). It is always safe to call.`;
576
915
  }
577
916
  }
578
917
  if (config.description) {
@@ -583,11 +922,62 @@ ${config.description}. This may evolve.`;
583
922
  }
584
923
  return prompt;
585
924
  }
925
+ function buildCliSystemPrompt(config, opts) {
926
+ return buildPrompt(config, "cli", opts);
927
+ }
928
+ function buildMcpSystemPrompt(config, opts) {
929
+ return buildPrompt(config, "mcp", opts);
930
+ }
931
+
932
+ // src/drivers/cliTransport.ts
933
+ var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
934
+ function buildCliTransportSystemPrompt(config, opts) {
935
+ return buildCliSystemPrompt(config, opts);
936
+ }
937
+ function prepareCliTransport(ctx, extraEnv = {}, platform = process.platform) {
938
+ if (!ctx.slockCliPath) {
939
+ throw new Error(`${ctx.config.runtime} driver: slockCliPath is required (daemon must inject it)`);
940
+ }
941
+ const slockDir = path.join(ctx.workingDirectory, ".slock");
942
+ mkdirSync(slockDir, { recursive: true });
943
+ const tokenFile = path.join(slockDir, "agent-token");
944
+ writeFileSync(tokenFile, ctx.config.authToken || ctx.daemonApiKey, { mode: 384 });
945
+ const posixWrapper = path.join(slockDir, "slock");
946
+ const posixBody = `#!/usr/bin/env bash
947
+ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)} "$@"
948
+ `;
949
+ writeFileSync(posixWrapper, posixBody, { mode: 493 });
950
+ if (platform === "win32") {
951
+ const cmdWrapper = path.join(slockDir, "slock.cmd");
952
+ const cmdBody = `@echo off\r
953
+ "${process.execPath}" "${ctx.slockCliPath}" %*\r
954
+ `;
955
+ writeFileSync(cmdWrapper, cmdBody);
956
+ }
957
+ const wrapperPath = platform === "win32" ? path.join(slockDir, "slock.cmd") : posixWrapper;
958
+ const spawnEnv = {
959
+ ...process.env,
960
+ FORCE_COLOR: "0",
961
+ ...ctx.config.envVars || {},
962
+ ...extraEnv,
963
+ SLOCK_AGENT_ID: ctx.agentId,
964
+ SLOCK_SERVER_URL: ctx.config.serverUrl,
965
+ SLOCK_AGENT_TOKEN_FILE: tokenFile,
966
+ PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
967
+ };
968
+ delete spawnEnv.SLOCK_AGENT_TOKEN;
969
+ return {
970
+ slockDir,
971
+ tokenFile,
972
+ wrapperPath,
973
+ spawnEnv
974
+ };
975
+ }
586
976
 
587
977
  // src/drivers/probe.ts
588
978
  import { execFileSync } from "child_process";
589
979
  import { existsSync } from "fs";
590
- import path from "path";
980
+ import path2 from "path";
591
981
  function normalizeExecOutput(raw) {
592
982
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
593
983
  }
@@ -653,12 +1043,19 @@ function readCommandVersion(command, args = [], deps = {}) {
653
1043
  }
654
1044
  function resolveHomePath(relativePath, deps = {}) {
655
1045
  const homeDir = deps.homeDir ?? deps.env?.HOME ?? process.env.HOME ?? "";
656
- return path.join(homeDir, relativePath);
1046
+ return path2.join(homeDir, relativePath);
657
1047
  }
658
1048
 
659
1049
  // src/drivers/claude.ts
660
- var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path2.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
1050
+ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
661
1051
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1052
+ var CLAUDE_DISALLOWED_TOOLS = [
1053
+ "EnterPlanMode",
1054
+ "ExitPlanMode",
1055
+ "CronCreate",
1056
+ "CronList",
1057
+ "CronDelete"
1058
+ ].join(",");
662
1059
  function resolveClaudeCommand(deps = {}) {
663
1060
  const pathCommand = resolveCommandOnPath("claude", deps);
664
1061
  if (pathCommand) return pathCommand;
@@ -681,36 +1078,11 @@ var ClaudeDriver = class {
681
1078
  supportsStdinNotification = true;
682
1079
  mcpToolPrefix = "mcp__chat__";
683
1080
  busyDeliveryMode = "notification";
1081
+ supportsNativeStandingPrompt = true;
684
1082
  probe() {
685
1083
  return probeClaude();
686
1084
  }
687
- spawn(ctx) {
688
- const mcpArgs = [
689
- ctx.chatBridgePath,
690
- "--agent-id",
691
- ctx.agentId,
692
- "--server-url",
693
- ctx.config.serverUrl,
694
- "--auth-token",
695
- ctx.config.authToken || ctx.daemonApiKey
696
- ];
697
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
698
- const mcpConfig = JSON.stringify({
699
- mcpServers: {
700
- chat: {
701
- command: isTsSource ? "npx" : "node",
702
- args: isTsSource ? ["tsx", ...mcpArgs] : mcpArgs
703
- }
704
- }
705
- });
706
- let mcpConfigArg;
707
- if (process.platform === "win32") {
708
- const mcpConfigPath = path2.join(ctx.workingDirectory, ".slock-claude-mcp.json");
709
- writeFileSync(mcpConfigPath, mcpConfig, "utf8");
710
- mcpConfigArg = mcpConfigPath;
711
- } else {
712
- mcpConfigArg = mcpConfig;
713
- }
1085
+ buildClaudeArgs(config, standingPrompt) {
714
1086
  const args = [
715
1087
  "--allow-dangerously-skip-permissions",
716
1088
  "--dangerously-skip-permissions",
@@ -719,18 +1091,25 @@ var ClaudeDriver = class {
719
1091
  "stream-json",
720
1092
  "--input-format",
721
1093
  "stream-json",
722
- "--mcp-config",
723
- mcpConfigArg,
1094
+ "--append-system-prompt",
1095
+ standingPrompt,
724
1096
  "--model",
725
- ctx.config.model || "sonnet",
1097
+ config.model || "sonnet",
726
1098
  "--disallowed-tools",
727
- "EnterPlanMode,ExitPlanMode"
1099
+ CLAUDE_DISALLOWED_TOOLS
728
1100
  ];
729
- if (ctx.config.sessionId) {
730
- args.push("--resume", ctx.config.sessionId);
1101
+ if (config.sessionId) {
1102
+ args.push("--resume", config.sessionId);
731
1103
  }
732
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", ...ctx.config.envVars || {} };
1104
+ return args;
1105
+ }
1106
+ spawn(ctx) {
1107
+ const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
1108
+ const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt);
733
1109
  delete spawnEnv.CLAUDECODE;
1110
+ logger.info(
1111
+ `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
1112
+ );
734
1113
  const proc = spawn(resolveClaudeCommand() ?? "claude", args, {
735
1114
  cwd: ctx.workingDirectory,
736
1115
  stdio: ["pipe", "pipe", "pipe"],
@@ -774,6 +1153,12 @@ var ClaudeDriver = class {
774
1153
  if (event.subtype === "init" && event.session_id) {
775
1154
  events.push({ kind: "session_init", sessionId: event.session_id });
776
1155
  }
1156
+ if (event.subtype === "status" && event.status === "compacting") {
1157
+ events.push({ kind: "compaction_started" });
1158
+ }
1159
+ if (event.subtype === "compact_boundary") {
1160
+ events.push({ kind: "compaction_finished" });
1161
+ }
777
1162
  break;
778
1163
  case "assistant": {
779
1164
  const content = event.message?.content;
@@ -831,11 +1216,9 @@ var ClaudeDriver = class {
831
1216
  });
832
1217
  }
833
1218
  buildSystemPrompt(config, _agentId) {
834
- return buildBaseSystemPrompt(config, {
1219
+ return buildCliTransportSystemPrompt(config, {
835
1220
  toolPrefix: "mcp__chat__",
836
- extraCriticalRules: [
837
- "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
838
- ],
1221
+ extraCriticalRules: [],
839
1222
  postStartupNotes: [],
840
1223
  includeStdinNotificationSection: true,
841
1224
  messageNotificationStyle: "poll"
@@ -847,7 +1230,7 @@ var ClaudeDriver = class {
847
1230
  import { spawn as spawn2, execSync } from "child_process";
848
1231
  import { existsSync as existsSync2, readFileSync } from "fs";
849
1232
  import os from "os";
850
- import path3 from "path";
1233
+ import path4 from "path";
851
1234
  function getCodexNotificationErrorMessage(params) {
852
1235
  const topLevelMessage = params?.message;
853
1236
  if (typeof topLevelMessage === "string" && topLevelMessage.trim()) {
@@ -859,21 +1242,19 @@ function getCodexNotificationErrorMessage(params) {
859
1242
  }
860
1243
  return null;
861
1244
  }
862
- function ensureGitRepo(workingDirectory) {
863
- const gitDir = path3.join(workingDirectory, ".git");
864
- if (existsSync2(gitDir)) return;
865
- execSync("git init", { cwd: workingDirectory, stdio: "pipe" });
866
- execSync("git add -A && git commit --allow-empty -m 'init'", {
867
- cwd: workingDirectory,
868
- stdio: "pipe",
869
- env: {
870
- ...process.env,
871
- GIT_AUTHOR_NAME: "slock",
872
- GIT_AUTHOR_EMAIL: "slock@local",
873
- GIT_COMMITTER_NAME: "slock",
874
- GIT_COMMITTER_EMAIL: "slock@local"
875
- }
876
- });
1245
+ function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1246
+ const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1247
+ const execSyncFn = deps.execSyncFn ?? execSync;
1248
+ const gitDir = path4.join(workingDirectory, ".git");
1249
+ if (existsSyncFn(gitDir)) return;
1250
+ execSyncFn("git init", { cwd: workingDirectory, stdio: "pipe" });
1251
+ execSyncFn(
1252
+ "git -c user.name=slock -c user.email=slock@local -c commit.gpgsign=false add -A && git -c user.name=slock -c user.email=slock@local -c commit.gpgsign=false commit --allow-empty -m 'init'",
1253
+ {
1254
+ cwd: workingDirectory,
1255
+ stdio: "pipe"
1256
+ }
1257
+ );
877
1258
  }
878
1259
  var CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
879
1260
  function resolveCodexCommand(deps = {}) {
@@ -908,14 +1289,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
908
1289
  let codexEntry = null;
909
1290
  try {
910
1291
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
911
- const candidate = path3.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1292
+ const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
912
1293
  if (existsSync2(candidate)) codexEntry = candidate;
913
1294
  } catch {
914
1295
  }
915
1296
  if (!codexEntry) {
916
1297
  try {
917
1298
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
918
- const candidate = path3.join(path3.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1299
+ const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
919
1300
  if (existsSync2(candidate)) codexEntry = candidate;
920
1301
  } catch {
921
1302
  }
@@ -930,12 +1311,6 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
930
1311
  args: [codexEntry, ...commandArgs]
931
1312
  };
932
1313
  }
933
- function buildBridgeArgs(ctx) {
934
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
935
- const command = isTsSource ? "npx" : "node";
936
- const args = 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];
937
- return { command, args };
938
- }
939
1314
  function joinReasoningText(item) {
940
1315
  const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
941
1316
  const content = Array.isArray(item.content) ? item.content.filter((entry) => typeof entry === "string") : [];
@@ -946,9 +1321,37 @@ var CodexDriver = class {
946
1321
  supportsStdinNotification = true;
947
1322
  mcpToolPrefix = "mcp_chat_";
948
1323
  busyDeliveryMode = "direct";
1324
+ supportsNativeStandingPrompt = true;
949
1325
  probe() {
950
1326
  return probeCodex();
951
1327
  }
1328
+ buildThreadRequest(ctx) {
1329
+ const threadParams = {
1330
+ cwd: ctx.workingDirectory,
1331
+ approvalPolicy: "never",
1332
+ sandbox: "danger-full-access",
1333
+ developerInstructions: ctx.standingPrompt
1334
+ };
1335
+ if (ctx.config.model) {
1336
+ threadParams.model = ctx.config.model;
1337
+ }
1338
+ if (ctx.config.reasoningEffort) {
1339
+ threadParams.config = { model_reasoning_effort: ctx.config.reasoningEffort };
1340
+ }
1341
+ if (ctx.config.sessionId) {
1342
+ return {
1343
+ method: "thread/resume",
1344
+ params: {
1345
+ threadId: ctx.config.sessionId,
1346
+ ...threadParams
1347
+ }
1348
+ };
1349
+ }
1350
+ return {
1351
+ method: "thread/start",
1352
+ params: threadParams
1353
+ };
1354
+ }
952
1355
  process = null;
953
1356
  requestId = 0;
954
1357
  threadId = null;
@@ -961,7 +1364,8 @@ var CodexDriver = class {
961
1364
  streamedAgentMessageIds = /* @__PURE__ */ new Set();
962
1365
  streamedReasoningIds = /* @__PURE__ */ new Set();
963
1366
  spawn(ctx) {
964
- ensureGitRepo(ctx.workingDirectory);
1367
+ ensureGitRepoForCodex(ctx.workingDirectory);
1368
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" });
965
1369
  this.process = null;
966
1370
  this.requestId = 0;
967
1371
  this.threadId = ctx.config.sessionId || null;
@@ -973,26 +1377,8 @@ var CodexDriver = class {
973
1377
  this.sessionAnnounced = false;
974
1378
  this.streamedAgentMessageIds.clear();
975
1379
  this.streamedReasoningIds.clear();
976
- const bridge = buildBridgeArgs(ctx);
977
- const args = [
978
- "app-server",
979
- "--listen",
980
- "stdio://",
981
- "-c",
982
- `mcp_servers.chat.command=${JSON.stringify(bridge.command)}`,
983
- "-c",
984
- `mcp_servers.chat.args=${JSON.stringify(bridge.args)}`,
985
- "-c",
986
- "mcp_servers.chat.startup_timeout_sec=30",
987
- "-c",
988
- "mcp_servers.chat.tool_timeout_sec=300",
989
- "-c",
990
- "mcp_servers.chat.enabled=true",
991
- "-c",
992
- "mcp_servers.chat.required=true"
993
- ];
1380
+ const args = ["app-server", "--listen", "stdio://"];
994
1381
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
995
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
996
1382
  const proc = spawn2(command, spawnArgs, {
997
1383
  cwd: ctx.workingDirectory,
998
1384
  stdio: ["pipe", "pipe", "pipe"],
@@ -1005,31 +1391,7 @@ var CodexDriver = class {
1005
1391
  clientInfo: { name: "slock-daemon", version: "1.0.0" },
1006
1392
  capabilities: { experimentalApi: true }
1007
1393
  });
1008
- const threadParams = {
1009
- cwd: ctx.workingDirectory,
1010
- approvalPolicy: "never",
1011
- sandbox: "danger-full-access"
1012
- };
1013
- if (ctx.config.model) {
1014
- threadParams.model = ctx.config.model;
1015
- }
1016
- if (ctx.config.reasoningEffort) {
1017
- threadParams.config = { model_reasoning_effort: ctx.config.reasoningEffort };
1018
- }
1019
- if (ctx.config.sessionId) {
1020
- this.pendingThreadRequest = {
1021
- method: "thread/resume",
1022
- params: {
1023
- threadId: ctx.config.sessionId,
1024
- ...threadParams
1025
- }
1026
- };
1027
- } else {
1028
- this.pendingThreadRequest = {
1029
- method: "thread/start",
1030
- params: threadParams
1031
- };
1032
- }
1394
+ this.pendingThreadRequest = this.buildThreadRequest(ctx);
1033
1395
  });
1034
1396
  return { process: proc };
1035
1397
  }
@@ -1145,6 +1507,14 @@ var CodexDriver = class {
1145
1507
  events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
1146
1508
  }
1147
1509
  break;
1510
+ case "contextCompaction":
1511
+ if (isStarted) {
1512
+ events.push({ kind: "compaction_started" });
1513
+ }
1514
+ if (isCompleted) {
1515
+ events.push({ kind: "compaction_finished" });
1516
+ }
1517
+ break;
1148
1518
  case "fileChange":
1149
1519
  if (isStarted && Array.isArray(item.changes)) {
1150
1520
  for (const change of item.changes) {
@@ -1225,11 +1595,9 @@ var CodexDriver = class {
1225
1595
  });
1226
1596
  }
1227
1597
  buildSystemPrompt(config, _agentId) {
1228
- return buildBaseSystemPrompt(config, {
1598
+ return buildCliTransportSystemPrompt(config, {
1229
1599
  toolPrefix: "",
1230
- extraCriticalRules: [
1231
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
1232
- ],
1600
+ extraCriticalRules: [],
1233
1601
  postStartupNotes: [
1234
1602
  "**IMPORTANT**: Your process stays alive across turns. New messages may be delivered directly into the current thread while you are working."
1235
1603
  ],
@@ -1281,8 +1649,8 @@ var CodexDriver = class {
1281
1649
  }
1282
1650
  };
1283
1651
  function detectCodexModels(home = os.homedir()) {
1284
- const cachePath = path3.join(home, ".codex", "models_cache.json");
1285
- const configPath = path3.join(home, ".codex", "config.toml");
1652
+ const cachePath = path4.join(home, ".codex", "models_cache.json");
1653
+ const configPath = path4.join(home, ".codex", "config.toml");
1286
1654
  let models = [];
1287
1655
  try {
1288
1656
  const raw = readFileSync(cachePath, "utf8");
@@ -1312,7 +1680,7 @@ function detectCodexModels(home = os.homedir()) {
1312
1680
 
1313
1681
  // src/drivers/copilot.ts
1314
1682
  import { spawn as spawn3 } from "child_process";
1315
- import path4 from "path";
1683
+ import path5 from "path";
1316
1684
  import { writeFileSync as writeFileSync2 } from "fs";
1317
1685
  var CopilotDriver = class {
1318
1686
  id = "copilot";
@@ -1327,7 +1695,7 @@ var CopilotDriver = class {
1327
1695
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1328
1696
  const mcpCommand = isTsSource ? "npx" : "node";
1329
1697
  const mcpArgs = 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];
1330
- const mcpConfigPath = path4.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1698
+ const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1331
1699
  writeFileSync2(mcpConfigPath, JSON.stringify({
1332
1700
  mcpServers: {
1333
1701
  chat: {
@@ -1436,7 +1804,7 @@ var CopilotDriver = class {
1436
1804
  return null;
1437
1805
  }
1438
1806
  buildSystemPrompt(config, _agentId) {
1439
- return buildBaseSystemPrompt(config, {
1807
+ return buildMcpSystemPrompt(config, {
1440
1808
  toolPrefix: "",
1441
1809
  extraCriticalRules: [
1442
1810
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1450,22 +1818,22 @@ var CopilotDriver = class {
1450
1818
 
1451
1819
  // src/drivers/cursor.ts
1452
1820
  import { spawn as spawn4 } from "child_process";
1453
- import { writeFileSync as writeFileSync3, mkdirSync, existsSync as existsSync3 } from "fs";
1454
- import path5 from "path";
1821
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1822
+ import path6 from "path";
1455
1823
  var CursorDriver = class {
1456
1824
  id = "cursor";
1457
1825
  supportsStdinNotification = false;
1458
1826
  mcpToolPrefix = "mcp__chat__";
1459
1827
  busyDeliveryMode = "none";
1460
1828
  spawn(ctx) {
1461
- const cursorDir = path5.join(ctx.workingDirectory, ".cursor");
1829
+ const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
1462
1830
  if (!existsSync3(cursorDir)) {
1463
- mkdirSync(cursorDir, { recursive: true });
1831
+ mkdirSync2(cursorDir, { recursive: true });
1464
1832
  }
1465
1833
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1466
1834
  const mcpCommand = isTsSource ? "npx" : "node";
1467
1835
  const mcpArgs = 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];
1468
- const mcpConfigPath = path5.join(cursorDir, "mcp.json");
1836
+ const mcpConfigPath = path6.join(cursorDir, "mcp.json");
1469
1837
  writeFileSync3(mcpConfigPath, JSON.stringify({
1470
1838
  mcpServers: {
1471
1839
  chat: {
@@ -1510,6 +1878,10 @@ var CursorDriver = class {
1510
1878
  case "system":
1511
1879
  if (event.subtype === "init" && event.session_id) {
1512
1880
  events.push({ kind: "session_init", sessionId: event.session_id });
1881
+ } else if (event.subtype === "status" && event.status === "compacting") {
1882
+ events.push({ kind: "compaction_started" });
1883
+ } else if (event.subtype === "compact_boundary") {
1884
+ events.push({ kind: "compaction_finished" });
1513
1885
  }
1514
1886
  break;
1515
1887
  case "assistant": {
@@ -1552,7 +1924,7 @@ var CursorDriver = class {
1552
1924
  return null;
1553
1925
  }
1554
1926
  buildSystemPrompt(config, _agentId) {
1555
- return buildBaseSystemPrompt(config, {
1927
+ return buildMcpSystemPrompt(config, {
1556
1928
  toolPrefix: "mcp__chat__",
1557
1929
  extraCriticalRules: [
1558
1930
  "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
@@ -1566,8 +1938,8 @@ var CursorDriver = class {
1566
1938
 
1567
1939
  // src/drivers/gemini.ts
1568
1940
  import { spawn as spawn5 } from "child_process";
1569
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1570
- import path6 from "path";
1941
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1942
+ import path7 from "path";
1571
1943
  var GeminiDriver = class {
1572
1944
  id = "gemini";
1573
1945
  supportsStdinNotification = false;
@@ -1578,14 +1950,14 @@ var GeminiDriver = class {
1578
1950
  spawn(ctx) {
1579
1951
  this.sessionId = ctx.config.sessionId || null;
1580
1952
  this.sessionAnnounced = false;
1581
- const geminiDir = path6.join(ctx.workingDirectory, ".gemini");
1953
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
1582
1954
  if (!existsSync4(geminiDir)) {
1583
- mkdirSync2(geminiDir, { recursive: true });
1955
+ mkdirSync3(geminiDir, { recursive: true });
1584
1956
  }
1585
1957
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1586
1958
  const mcpCommand = isTsSource ? "npx" : "node";
1587
1959
  const mcpArgs = 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];
1588
- const settingsPath = path6.join(geminiDir, "settings.json");
1960
+ const settingsPath = path7.join(geminiDir, "settings.json");
1589
1961
  writeFileSync4(settingsPath, JSON.stringify({
1590
1962
  mcpServers: {
1591
1963
  chat: {
@@ -1667,7 +2039,7 @@ var GeminiDriver = class {
1667
2039
  return null;
1668
2040
  }
1669
2041
  buildSystemPrompt(config, _agentId) {
1670
- return buildBaseSystemPrompt(config, {
2042
+ return buildMcpSystemPrompt(config, {
1671
2043
  toolPrefix: "",
1672
2044
  extraCriticalRules: [
1673
2045
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1684,7 +2056,7 @@ import { randomUUID } from "crypto";
1684
2056
  import { spawn as spawn6 } from "child_process";
1685
2057
  import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
1686
2058
  import os2 from "os";
1687
- import path7 from "path";
2059
+ import path8 from "path";
1688
2060
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
1689
2061
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
1690
2062
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
@@ -1713,9 +2085,9 @@ var KimiDriver = class {
1713
2085
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1714
2086
  const command = isTsSource ? "npx" : "node";
1715
2087
  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];
1716
- const systemPromptPath = path7.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
1717
- const agentFilePath = path7.join(ctx.workingDirectory, KIMI_AGENT_FILE);
1718
- const mcpConfigPath = path7.join(ctx.workingDirectory, KIMI_MCP_FILE);
2088
+ const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2089
+ const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2090
+ const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
1719
2091
  if (!isResume || !existsSync5(systemPromptPath)) {
1720
2092
  writeFileSync5(systemPromptPath, ctx.prompt, "utf8");
1721
2093
  }
@@ -1796,6 +2168,12 @@ var KimiDriver = class {
1796
2168
  case "StepBegin":
1797
2169
  events.push({ kind: "thinking", text: "" });
1798
2170
  break;
2171
+ case "CompactionBegin":
2172
+ events.push({ kind: "compaction_started" });
2173
+ break;
2174
+ case "CompactionEnd":
2175
+ events.push({ kind: "compaction_finished" });
2176
+ break;
1799
2177
  case "ContentPart":
1800
2178
  if (payload.type === "think" && payload.think) {
1801
2179
  events.push({ kind: "thinking", text: payload.think });
@@ -1848,7 +2226,7 @@ var KimiDriver = class {
1848
2226
  });
1849
2227
  }
1850
2228
  buildSystemPrompt(config, _agentId) {
1851
- return buildBaseSystemPrompt(config, {
2229
+ return buildMcpSystemPrompt(config, {
1852
2230
  toolPrefix: "",
1853
2231
  extraCriticalRules: [
1854
2232
  "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
@@ -1863,7 +2241,7 @@ var KimiDriver = class {
1863
2241
  }
1864
2242
  };
1865
2243
  function detectKimiModels(home = os2.homedir()) {
1866
- const configPath = path7.join(home, ".kimi", "config.toml");
2244
+ const configPath = path8.join(home, ".kimi", "config.toml");
1867
2245
  let raw;
1868
2246
  try {
1869
2247
  raw = readFileSync2(configPath, "utf8");
@@ -1908,7 +2286,7 @@ function getDriver(runtimeId) {
1908
2286
 
1909
2287
  // src/workspaces.ts
1910
2288
  import { readdir, rm, stat } from "fs/promises";
1911
- import path8 from "path";
2289
+ import path9 from "path";
1912
2290
  function isValidWorkspaceDirectoryName(directoryName) {
1913
2291
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
1914
2292
  }
@@ -1916,7 +2294,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
1916
2294
  if (!isValidWorkspaceDirectoryName(directoryName)) {
1917
2295
  return null;
1918
2296
  }
1919
- return path8.join(dataDir, directoryName);
2297
+ return path9.join(dataDir, directoryName);
1920
2298
  }
1921
2299
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
1922
2300
  return {
@@ -1965,7 +2343,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
1965
2343
  return summary;
1966
2344
  }
1967
2345
  const childSummaries = await Promise.all(
1968
- entries.map((entry) => summarizeWorkspaceEntry(path8.join(dirPath, entry.name), entry))
2346
+ entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
1969
2347
  );
1970
2348
  for (const childSummary of childSummaries) {
1971
2349
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -1984,7 +2362,7 @@ async function scanWorkspaceDirectories(dataDir) {
1984
2362
  if (!entry.isDirectory()) {
1985
2363
  return null;
1986
2364
  }
1987
- const dirPath = path8.join(dataDir, entry.name);
2365
+ const dirPath = path9.join(dataDir, entry.name);
1988
2366
  try {
1989
2367
  const summary = await summarizeWorkspaceDirectory(dirPath);
1990
2368
  return {
@@ -2016,7 +2394,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
2016
2394
  }
2017
2395
 
2018
2396
  // src/agentProcessManager.ts
2019
- var DATA_DIR = path9.join(os3.homedir(), ".slock", "agents");
2397
+ var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
2020
2398
  function toLocalTime(iso) {
2021
2399
  const d = new Date(iso);
2022
2400
  if (isNaN(d.getTime())) return iso;
@@ -2170,6 +2548,7 @@ function getBusyDeliveryNote(driver) {
2170
2548
  }
2171
2549
  return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
2172
2550
  }
2551
+ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
2173
2552
  var AgentProcessManager = class _AgentProcessManager {
2174
2553
  agents = /* @__PURE__ */ new Map();
2175
2554
  agentsStarting = /* @__PURE__ */ new Set();
@@ -2178,6 +2557,7 @@ var AgentProcessManager = class _AgentProcessManager {
2178
2557
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
2179
2558
  idleAgentConfigs = /* @__PURE__ */ new Map();
2180
2559
  chatBridgePath;
2560
+ slockCliPath;
2181
2561
  sendToServer;
2182
2562
  daemonApiKey;
2183
2563
  serverUrl;
@@ -2186,6 +2566,7 @@ var AgentProcessManager = class _AgentProcessManager {
2186
2566
  defaultAgentEnvVarsProvider;
2187
2567
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
2188
2568
  this.chatBridgePath = chatBridgePath;
2569
+ this.slockCliPath = opts.slockCliPath ?? "";
2189
2570
  this.sendToServer = sendToServer;
2190
2571
  this.daemonApiKey = daemonApiKey;
2191
2572
  this.serverUrl = opts.serverUrl;
@@ -2205,9 +2586,9 @@ var AgentProcessManager = class _AgentProcessManager {
2205
2586
  this.agentsStarting.add(agentId);
2206
2587
  try {
2207
2588
  const driver = this.driverResolver(config.runtime || "claude");
2208
- const agentDataDir = path9.join(this.dataDir, agentId);
2589
+ const agentDataDir = path10.join(this.dataDir, agentId);
2209
2590
  await mkdir(agentDataDir, { recursive: true });
2210
- const memoryMdPath = path9.join(agentDataDir, "MEMORY.md");
2591
+ const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
2211
2592
  try {
2212
2593
  await access(memoryMdPath);
2213
2594
  } catch {
@@ -2225,8 +2606,9 @@ ${config.description || "No role defined yet."}
2225
2606
  `;
2226
2607
  await writeFile(memoryMdPath, initialMemoryMd);
2227
2608
  }
2228
- await mkdir(path9.join(agentDataDir, "notes"), { recursive: true });
2609
+ await mkdir(path10.join(agentDataDir, "notes"), { recursive: true });
2229
2610
  const isResume = !!config.sessionId;
2611
+ const standingPrompt = driver.buildSystemPrompt(config, agentId);
2230
2612
  let prompt;
2231
2613
  if (isResume && resumePrompt) {
2232
2614
  prompt = resumePrompt;
@@ -2270,15 +2652,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2270
2652
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
2271
2653
  prompt += getBusyDeliveryNote(driver);
2272
2654
  } else {
2273
- prompt = driver.buildSystemPrompt(config, agentId);
2655
+ prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
2274
2656
  }
2275
2657
  const effectiveConfig = await this.buildSpawnConfig(agentId, config);
2276
2658
  const { process: proc } = driver.spawn({
2277
2659
  agentId,
2278
2660
  config: effectiveConfig,
2661
+ standingPrompt,
2279
2662
  prompt,
2280
2663
  workingDirectory: agentDataDir,
2281
2664
  chatBridgePath: this.chatBridgePath,
2665
+ slockCliPath: this.slockCliPath,
2282
2666
  daemonApiKey: this.daemonApiKey
2283
2667
  });
2284
2668
  const agentProcess = {
@@ -2566,7 +2950,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2566
2950
  }
2567
2951
  }
2568
2952
  async resetWorkspace(agentId) {
2569
- const agentDataDir = path9.join(this.dataDir, agentId);
2953
+ const agentDataDir = path10.join(this.dataDir, agentId);
2570
2954
  try {
2571
2955
  await rm2(agentDataDir, { recursive: true, force: true });
2572
2956
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -2604,7 +2988,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2604
2988
  }
2605
2989
  // Workspace file browsing
2606
2990
  async getFileTree(agentId, dirPath) {
2607
- const agentDir = path9.join(this.dataDir, agentId);
2991
+ const agentDir = path10.join(this.dataDir, agentId);
2608
2992
  try {
2609
2993
  await stat2(agentDir);
2610
2994
  } catch {
@@ -2612,8 +2996,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2612
2996
  }
2613
2997
  let targetDir = agentDir;
2614
2998
  if (dirPath) {
2615
- const resolved = path9.resolve(agentDir, dirPath);
2616
- if (!resolved.startsWith(agentDir + path9.sep) && resolved !== agentDir) {
2999
+ const resolved = path10.resolve(agentDir, dirPath);
3000
+ if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
2617
3001
  return [];
2618
3002
  }
2619
3003
  targetDir = resolved;
@@ -2621,9 +3005,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2621
3005
  return this.listDirectoryChildren(targetDir, agentDir);
2622
3006
  }
2623
3007
  async readFile(agentId, filePath) {
2624
- const agentDir = path9.join(this.dataDir, agentId);
2625
- const resolved = path9.resolve(agentDir, filePath);
2626
- if (!resolved.startsWith(agentDir + path9.sep) && resolved !== agentDir) {
3008
+ const agentDir = path10.join(this.dataDir, agentId);
3009
+ const resolved = path10.resolve(agentDir, filePath);
3010
+ if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
2627
3011
  throw new Error("Access denied");
2628
3012
  }
2629
3013
  const info = await stat2(resolved);
@@ -2647,7 +3031,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2647
3031
  ".sh",
2648
3032
  ".py"
2649
3033
  ]);
2650
- const ext = path9.extname(resolved).toLowerCase();
3034
+ const ext = path10.extname(resolved).toLowerCase();
2651
3035
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
2652
3036
  return { content: null, binary: true };
2653
3037
  }
@@ -2674,13 +3058,13 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2674
3058
  const agent = this.agents.get(agentId);
2675
3059
  const runtime = runtimeHint || agent?.config.runtime || "claude";
2676
3060
  const home = os3.homedir();
2677
- const workspaceDir = path9.join(this.dataDir, agentId);
3061
+ const workspaceDir = path10.join(this.dataDir, agentId);
2678
3062
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
2679
3063
  const globalResults = await Promise.all(
2680
- paths.global.map((p) => this.scanSkillsDir(path9.join(home, p)))
3064
+ paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
2681
3065
  );
2682
3066
  const workspaceResults = await Promise.all(
2683
- paths.workspace.map((p) => this.scanSkillsDir(path9.join(workspaceDir, p)))
3067
+ paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
2684
3068
  );
2685
3069
  const dedup = (skills) => {
2686
3070
  const seen = /* @__PURE__ */ new Set();
@@ -2709,7 +3093,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2709
3093
  const skills = [];
2710
3094
  for (const entry of entries) {
2711
3095
  if (entry.isDirectory() || entry.isSymbolicLink()) {
2712
- const skillMd = path9.join(dir, entry.name, "SKILL.md");
3096
+ const skillMd = path10.join(dir, entry.name, "SKILL.md");
2713
3097
  try {
2714
3098
  const content = await readFile(skillMd, "utf-8");
2715
3099
  const skill = this.parseSkillMd(entry.name, content);
@@ -2720,7 +3104,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2720
3104
  } else if (entry.name.endsWith(".md")) {
2721
3105
  const cmdName = entry.name.replace(/\.md$/, "");
2722
3106
  try {
2723
- const content = await readFile(path9.join(dir, entry.name), "utf-8");
3107
+ const content = await readFile(path10.join(dir, entry.name), "utf-8");
2724
3108
  const skill = this.parseSkillMd(cmdName, content);
2725
3109
  skill.sourcePath = dir;
2726
3110
  skills.push(skill);
@@ -2852,13 +3236,27 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2852
3236
  }
2853
3237
  case "tool_call": {
2854
3238
  this.flushPendingTrajectory(agentId);
2855
- const toolName = event.name;
2856
- const inputSummary = summarizeToolInput(toolName, event.input);
2857
- const detail = getToolActivityLabel(toolName);
2858
- this.broadcastActivity(agentId, "working", detail, [{ kind: "tool_start", toolName, toolInput: inputSummary }]);
3239
+ const invocation = normalizeToolDisplayInvocation(event.name, event.input);
3240
+ const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
3241
+ const detail = getToolActivityLabel(invocation.toolName);
3242
+ this.broadcastActivity(agentId, "working", detail, [{
3243
+ kind: "tool_start",
3244
+ toolName: invocation.toolName,
3245
+ toolInput: inputSummary
3246
+ }]);
2859
3247
  if (ap) ap.isIdle = false;
2860
3248
  break;
2861
3249
  }
3250
+ case "compaction_started":
3251
+ this.flushPendingTrajectory(agentId);
3252
+ this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
3253
+ if (ap) ap.isIdle = false;
3254
+ break;
3255
+ case "compaction_finished":
3256
+ this.flushPendingTrajectory(agentId);
3257
+ this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
3258
+ if (ap) ap.isIdle = false;
3259
+ break;
2862
3260
  case "turn_end":
2863
3261
  this.flushPendingTrajectory(agentId);
2864
3262
  if (ap) {
@@ -2965,8 +3363,8 @@ Respond as appropriate. Complete all your work before stopping.`;
2965
3363
  const nodes = [];
2966
3364
  for (const entry of entries) {
2967
3365
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2968
- const fullPath = path9.join(dir, entry.name);
2969
- const relativePath = path9.relative(rootDir, fullPath);
3366
+ const fullPath = path10.join(dir, entry.name);
3367
+ const relativePath = path10.relative(rootDir, fullPath);
2970
3368
  let info;
2971
3369
  try {
2972
3370
  info = await stat2(fullPath);
@@ -3116,6 +3514,85 @@ var DaemonConnection = class {
3116
3514
  }
3117
3515
  };
3118
3516
 
3517
+ // src/reminderCache.ts
3518
+ var DEFAULT_MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
3519
+ var ReminderCache = class {
3520
+ entries = /* @__PURE__ */ new Map();
3521
+ clock;
3522
+ onFire;
3523
+ maxDelayMs;
3524
+ constructor(opts) {
3525
+ this.clock = opts.clock ?? systemClock;
3526
+ this.onFire = opts.onFire;
3527
+ this.maxDelayMs = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
3528
+ }
3529
+ upsert(job) {
3530
+ const existing = this.entries.get(job.reminderId);
3531
+ if (existing && existing.job.version >= job.version) {
3532
+ logger.info(`[ReminderCache] Stale upsert for ${job.reminderId} (incoming v${job.version} <= cached v${existing.job.version}) \u2014 ignored`);
3533
+ return;
3534
+ }
3535
+ if (existing?.timer) this.clock.clearTimeout(existing.timer);
3536
+ const timer = this.scheduleTimer(job);
3537
+ this.entries.set(job.reminderId, { job, timer });
3538
+ }
3539
+ cancel(reminderId, version) {
3540
+ const existing = this.entries.get(reminderId);
3541
+ if (!existing) return;
3542
+ if (existing.job.version > version) {
3543
+ logger.info(`[ReminderCache] Stale cancel for ${reminderId} (incoming v${version} < cached v${existing.job.version}) \u2014 ignored`);
3544
+ return;
3545
+ }
3546
+ if (existing.timer) this.clock.clearTimeout(existing.timer);
3547
+ this.entries.delete(reminderId);
3548
+ }
3549
+ snapshot(agentId, jobs) {
3550
+ for (const [reminderId, entry] of this.entries) {
3551
+ if (entry.job.ownerAgentId !== agentId) continue;
3552
+ if (entry.timer) this.clock.clearTimeout(entry.timer);
3553
+ this.entries.delete(reminderId);
3554
+ }
3555
+ for (const job of jobs) {
3556
+ if (job.ownerAgentId !== agentId) {
3557
+ logger.warn(
3558
+ `[ReminderCache] snapshot for agent ${agentId} carried job ${job.reminderId} owned by ${job.ownerAgentId} \u2014 skipping`
3559
+ );
3560
+ continue;
3561
+ }
3562
+ const timer = this.scheduleTimer(job);
3563
+ this.entries.set(job.reminderId, { job, timer });
3564
+ }
3565
+ }
3566
+ clear() {
3567
+ for (const entry of this.entries.values()) {
3568
+ if (entry.timer) this.clock.clearTimeout(entry.timer);
3569
+ }
3570
+ this.entries.clear();
3571
+ }
3572
+ size() {
3573
+ return this.entries.size;
3574
+ }
3575
+ getJob(reminderId) {
3576
+ return this.entries.get(reminderId)?.job ?? null;
3577
+ }
3578
+ scheduleTimer(job) {
3579
+ const fireAt = Date.parse(job.fireAt);
3580
+ if (Number.isNaN(fireAt)) {
3581
+ logger.warn(`[ReminderCache] Invalid fireAt for ${job.reminderId}: ${job.fireAt}`);
3582
+ return null;
3583
+ }
3584
+ const delay = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
3585
+ return this.clock.setTimeout(() => {
3586
+ this.entries.delete(job.reminderId);
3587
+ try {
3588
+ this.onFire(job);
3589
+ } catch (err) {
3590
+ logger.error(`[ReminderCache] onFire threw for ${job.reminderId}`, err);
3591
+ }
3592
+ }, delay);
3593
+ }
3594
+ };
3595
+
3119
3596
  // src/core.ts
3120
3597
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
3121
3598
  function parseDaemonCliArgs(args) {
@@ -3137,13 +3614,25 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
3137
3614
  }
3138
3615
  }
3139
3616
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
3140
- const dirname = path10.dirname(fileURLToPath(moduleUrl));
3141
- const jsPath = path10.resolve(dirname, "chat-bridge.js");
3617
+ const dirname = path11.dirname(fileURLToPath(moduleUrl));
3618
+ const jsPath = path11.resolve(dirname, "chat-bridge.js");
3142
3619
  try {
3143
3620
  accessSync(jsPath);
3144
3621
  return jsPath;
3145
3622
  } catch {
3146
- return path10.resolve(dirname, "chat-bridge.ts");
3623
+ return path11.resolve(dirname, "chat-bridge.ts");
3624
+ }
3625
+ }
3626
+ function resolveSlockCliPath(moduleUrl = import.meta.url) {
3627
+ const thisDir = path11.dirname(fileURLToPath(moduleUrl));
3628
+ const bundledDistPath = path11.resolve(thisDir, "cli", "index.js");
3629
+ try {
3630
+ accessSync(bundledDistPath);
3631
+ return bundledDistPath;
3632
+ } catch {
3633
+ const workspaceDistPath = path11.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
3634
+ accessSync(workspaceDistPath);
3635
+ return workspaceDistPath;
3147
3636
  }
3148
3637
  }
3149
3638
  function detectRuntimes() {
@@ -3196,6 +3685,12 @@ function summarizeIncomingMessage(msg) {
3196
3685
  return `(directory=${msg.directoryName})`;
3197
3686
  case "machine:runtime_models:detect":
3198
3687
  return `(runtime=${msg.runtime}, req=${msg.requestId})`;
3688
+ case "reminder.upsert":
3689
+ return `(agent=${msg.agentId}, id=${msg.reminder.reminderId}, v${msg.reminder.version}, fireAt=${msg.reminder.fireAt})`;
3690
+ case "reminder.cancel":
3691
+ return `(agent=${msg.agentId}, id=${msg.reminderId}, v${msg.version})`;
3692
+ case "reminder.snapshot":
3693
+ return `(agent=${msg.agentId}, count=${msg.reminders.length})`;
3199
3694
  default:
3200
3695
  return "";
3201
3696
  }
@@ -3204,19 +3699,27 @@ var DaemonCore = class {
3204
3699
  options;
3205
3700
  daemonVersion;
3206
3701
  chatBridgePath;
3702
+ slockCliPath;
3207
3703
  runtimeDetector;
3208
3704
  agentManager;
3209
3705
  connection;
3706
+ reminderCache;
3210
3707
  constructor(options) {
3211
3708
  this.options = options;
3212
3709
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
3213
3710
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
3711
+ this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
3214
3712
  this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
3713
+ this.reminderCache = new ReminderCache({
3714
+ clock: options.reminderClock,
3715
+ onFire: (job) => this.onReminderFire(job)
3716
+ });
3215
3717
  let connection;
3216
3718
  const agentManagerOptions = {
3217
3719
  dataDir: options.dataDir,
3218
3720
  serverUrl: options.serverUrl,
3219
- defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider
3721
+ defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
3722
+ slockCliPath: this.slockCliPath
3220
3723
  };
3221
3724
  this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
3222
3725
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -3236,6 +3739,7 @@ var DaemonCore = class {
3236
3739
  }
3237
3740
  async stop() {
3238
3741
  logger.info("[Slock Daemon] Shutting down...");
3742
+ this.reminderCache.clear();
3239
3743
  await this.agentManager.stopAll();
3240
3744
  this.connection.disconnect();
3241
3745
  }
@@ -3330,11 +3834,31 @@ var DaemonCore = class {
3330
3834
  });
3331
3835
  break;
3332
3836
  }
3837
+ case "reminder.upsert":
3838
+ this.reminderCache.upsert(msg.reminder);
3839
+ break;
3840
+ case "reminder.cancel":
3841
+ this.reminderCache.cancel(msg.reminderId, msg.version);
3842
+ break;
3843
+ case "reminder.snapshot":
3844
+ logger.info(`[Daemon] Reminder snapshot for agent ${msg.agentId}: ${msg.reminders.length} entries`);
3845
+ this.reminderCache.snapshot(msg.agentId, msg.reminders);
3846
+ break;
3333
3847
  case "ping":
3334
3848
  this.connection.send({ type: "pong" });
3335
3849
  break;
3336
3850
  }
3337
3851
  }
3852
+ onReminderFire(job) {
3853
+ logger.info(`[Daemon] Reminder ${job.reminderId} fired locally (agent=${job.ownerAgentId})`);
3854
+ this.connection.send({
3855
+ type: "reminder.fire_attempt",
3856
+ agentId: job.ownerAgentId,
3857
+ reminderId: job.reminderId,
3858
+ version: job.version,
3859
+ firedAtClient: (/* @__PURE__ */ new Date()).toISOString()
3860
+ });
3861
+ }
3338
3862
  handleConnect() {
3339
3863
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
3340
3864
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
@@ -3358,6 +3882,13 @@ var DaemonCore = class {
3358
3882
  for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
3359
3883
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
3360
3884
  }
3885
+ const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
3886
+ for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
3887
+ agentsForSnapshot.add(agentId);
3888
+ }
3889
+ for (const agentId of agentsForSnapshot) {
3890
+ this.connection.send({ type: "reminder.snapshot.request", agentId });
3891
+ }
3361
3892
  this.options.lifecycleHooks?.onConnect?.();
3362
3893
  }
3363
3894
  handleDisconnect() {
@@ -3374,6 +3905,7 @@ export {
3374
3905
  parseDaemonCliArgs,
3375
3906
  readDaemonVersion,
3376
3907
  resolveChatBridgePath,
3908
+ resolveSlockCliPath,
3377
3909
  detectRuntimes,
3378
3910
  DaemonCore
3379
3911
  };