@slock-ai/daemon 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import {
4
4
  } from "./chunk-JG7ONJZ6.js";
5
5
 
6
6
  // src/core.ts
7
- import path11 from "path";
8
- import os4 from "os";
7
+ import path12 from "path";
8
+ import os5 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
11
11
  import { fileURLToPath } from "url";
@@ -582,6 +582,7 @@ var DISPLAY_PLAN_CONFIG = {
582
582
  };
583
583
 
584
584
  // src/agentProcessManager.ts
585
+ import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
585
586
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
586
587
  import path10 from "path";
587
588
  import os3 from "os";
@@ -599,6 +600,27 @@ import path from "path";
599
600
  function toolRef(prefix, name) {
600
601
  return `${prefix}${name}`;
601
602
  }
603
+ function runtimeContextLines(config) {
604
+ const ctx = config.runtimeContext;
605
+ if (!ctx) return [];
606
+ const lines = [
607
+ "## Current Runtime Context",
608
+ "",
609
+ "This is authoritative context injected by Slock. Do not infer computer identity from hostname or cwd when this section is present.",
610
+ ""
611
+ ];
612
+ if (ctx.agentId) lines.push(`- Agent ID: ${ctx.agentId}`);
613
+ if (ctx.serverId) lines.push(`- Server ID: ${ctx.serverId}`);
614
+ if (ctx.machineName || ctx.machineId) {
615
+ const label = ctx.machineName && ctx.machineId ? `${ctx.machineName} (${ctx.machineId})` : ctx.machineName || ctx.machineId;
616
+ lines.push(`- Computer: ${label}`);
617
+ }
618
+ if (ctx.machineHostname) lines.push(`- Hostname: ${ctx.machineHostname}`);
619
+ if (ctx.machineOs) lines.push(`- OS: ${ctx.machineOs}`);
620
+ if (ctx.daemonVersion) lines.push(`- Daemon: v${ctx.daemonVersion}`);
621
+ if (ctx.workspacePath) lines.push(`- Workspace: ${ctx.workspacePath}`);
622
+ return lines.length > 4 ? lines : [];
623
+ }
602
624
  function buildPrompt(config, variant, opts) {
603
625
  const isCli = variant === "cli";
604
626
  const t = (name) => toolRef(opts.toolPrefix, name);
@@ -621,13 +643,18 @@ function buildPrompt(config, variant, opts) {
621
643
  "- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
622
644
  `- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
623
645
  ];
646
+ const runtimeProfileControlStartupStep = config.runtimeProfileControl ? [
647
+ "0. If this system prompt contains a **Runtime Profile Control** section, complete that runtime-control instruction first. Do not read MEMORY.md, check messages, or respond to inbox messages until the required runtime control action has succeeded."
648
+ ] : [];
624
649
  const startupSteps = isCli ? [
650
+ ...runtimeProfileControlStartupStep,
625
651
  "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.",
626
652
  "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
627
653
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
628
654
  "4. When you receive a message, process it and reply with `slock message send`.",
629
655
  "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."
630
656
  ] : [
657
+ ...runtimeProfileControlStartupStep,
631
658
  `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.`,
632
659
  "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
633
660
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
@@ -641,20 +668,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
641
668
  1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
642
669
  2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
643
670
  3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
644
- 4. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
645
- 5. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
646
- 6. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
647
- 7. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
648
- 8. **\`slock task list\`** \u2014 View a channel's task board.
649
- 9. **\`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).
650
- 10. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
651
- 11. **\`slock task unclaim\`** \u2014 Release your claim on a task.
652
- 12. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
653
- 13. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
654
- 14. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
655
- 15. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
656
- 16. **\`slock reminder list\`** \u2014 List your reminders.
657
- 17. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
671
+ 4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
672
+ 5. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
673
+ 6. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
674
+ 7. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
675
+ 8. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
676
+ 9. **\`slock task list\`** \u2014 View a channel's task board.
677
+ 10. **\`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).
678
+ 11. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
679
+ 12. **\`slock task unclaim\`** \u2014 Release your claim on a task.
680
+ 13. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
681
+ 14. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Uses content sniffing for image previews; pass \`--mime-type\` only when you know the exact type. Returns an attachment ID to pass to \`slock message send\`.
682
+ 15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
683
+ 16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
684
+ 17. **\`slock profile update\`** \u2014 Update your own profile. Currently this only supports \`--avatar-file\`.
685
+ 18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
686
+ 19. **\`slock reminder list\`** \u2014 List your reminders.
687
+ 20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
658
688
 
659
689
  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.
660
690
  Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
@@ -728,7 +758,7 @@ Threads are sub-conversations attached to a specific message. They let you discu
728
758
  const discoverySection = isCli ? `### Discovering people and channels
729
759
 
730
760
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
731
- 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. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
761
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
732
762
 
733
763
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
734
764
  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. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
@@ -838,6 +868,8 @@ ${readCmd} shows messages in their current state. If a message was later convert
838
868
 
839
869
  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.
840
870
 
871
+ ${runtimeContextLines(config).join("\n")}
872
+
841
873
  ${communicationSection}
842
874
 
843
875
  CRITICAL RULES:
@@ -851,6 +883,30 @@ ${startupSteps.join("\n")}`;
851
883
 
852
884
  ${opts.postStartupNotes.join("\n")}`;
853
885
  }
886
+ if (config.runtimeProfileControl) {
887
+ const control = config.runtimeProfileControl;
888
+ prompt += `
889
+
890
+ ## Runtime Profile Control
891
+
892
+ `;
893
+ prompt += `This section is a trusted daemon runtime-control instruction. It overrides the normal startup sequence: complete this section before reading MEMORY.md, checking messages, or responding to inbox messages.
894
+
895
+ `;
896
+ if (control.kind === "migration") {
897
+ prompt += `You are currently in Runtime Profile migration. Before handling normal inbox messages, re-ground yourself in the new runtime context and then invoke the runtime control action \`${t("runtime_profile_migration_done")}\` with exactly this migration_key: \`${control.key}\`.
898
+
899
+ `;
900
+ prompt += `Do not use ${sendCmd}, ${checkCmd}, or any chat reply as the migration acknowledgment. Normal inbox delivery is gated until the runtime control action succeeds.
901
+
902
+ `;
903
+ } else {
904
+ prompt += `Read the daemon release notice below before handling normal inbox messages. No chat reply is required for this notice.
905
+
906
+ `;
907
+ }
908
+ prompt += control.message;
909
+ }
854
910
  prompt += `
855
911
 
856
912
  ## Messaging
@@ -1045,6 +1101,20 @@ function buildMcpSystemPrompt(config, opts) {
1045
1101
 
1046
1102
  // src/drivers/cliTransport.ts
1047
1103
  var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1104
+ function runtimeContextEnv(config) {
1105
+ const ctx = config.runtimeContext;
1106
+ if (!ctx) return {};
1107
+ return {
1108
+ ...ctx.agentId ? { SLOCK_CURRENT_AGENT_ID: ctx.agentId } : {},
1109
+ ...ctx.serverId ? { SLOCK_CURRENT_SERVER_ID: ctx.serverId } : {},
1110
+ ...ctx.machineId ? { SLOCK_CURRENT_COMPUTER_ID: ctx.machineId } : {},
1111
+ ...ctx.machineName ? { SLOCK_CURRENT_COMPUTER_NAME: ctx.machineName } : {},
1112
+ ...ctx.machineHostname ? { SLOCK_CURRENT_COMPUTER_HOSTNAME: ctx.machineHostname } : {},
1113
+ ...ctx.machineOs ? { SLOCK_CURRENT_COMPUTER_OS: ctx.machineOs } : {},
1114
+ ...ctx.daemonVersion ? { SLOCK_CURRENT_DAEMON_VERSION: ctx.daemonVersion } : {},
1115
+ ...ctx.workspacePath ? { SLOCK_CURRENT_WORKSPACE_PATH: ctx.workspacePath } : {}
1116
+ };
1117
+ }
1048
1118
  function buildCliTransportSystemPrompt(config, opts) {
1049
1119
  return buildCliSystemPrompt(config, opts);
1050
1120
  }
@@ -1074,6 +1144,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1074
1144
  FORCE_COLOR: "0",
1075
1145
  ...ctx.config.envVars || {},
1076
1146
  ...extraEnv,
1147
+ ...runtimeContextEnv(ctx.config),
1077
1148
  SLOCK_AGENT_ID: ctx.agentId,
1078
1149
  ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
1079
1150
  SLOCK_SERVER_URL: ctx.config.serverUrl,
@@ -1097,7 +1168,7 @@ function normalizeExecOutput(raw) {
1097
1168
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
1098
1169
  }
1099
1170
  function resolveCommandOnWindows(command, env, execFileSyncFn) {
1100
- const script = "$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition }";
1171
+ const script = "& {$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition } }";
1101
1172
  try {
1102
1173
  const output = normalizeExecOutput(execFileSyncFn("powershell.exe", [
1103
1174
  "-NoProfile",
@@ -1207,6 +1278,8 @@ var ClaudeDriver = class {
1207
1278
  "--allow-dangerously-skip-permissions",
1208
1279
  "--dangerously-skip-permissions",
1209
1280
  "--verbose",
1281
+ "--permission-mode",
1282
+ "bypassPermissions",
1210
1283
  "--output-format",
1211
1284
  "stream-json",
1212
1285
  "--input-format",
@@ -1217,16 +1290,16 @@ var ClaudeDriver = class {
1217
1290
  CLAUDE_DISALLOWED_TOOLS
1218
1291
  ];
1219
1292
  if (opts.standingPromptFilePath) {
1220
- args.push("--append-system-prompt-file", opts.standingPromptFilePath);
1293
+ args.push("--system-prompt-file", opts.standingPromptFilePath);
1221
1294
  } else {
1222
- args.push("--append-system-prompt", standingPrompt);
1295
+ args.push("--system-prompt", standingPrompt);
1223
1296
  }
1224
1297
  if (config.sessionId) {
1225
1298
  args.push("--resume", config.sessionId);
1226
1299
  }
1227
1300
  return args;
1228
1301
  }
1229
- buildDeprecatedShimMcpConfig(ctx) {
1302
+ buildRuntimeActionsMcpConfig(ctx) {
1230
1303
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1231
1304
  const command = isTsSource ? "npx" : "node";
1232
1305
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
@@ -1245,7 +1318,7 @@ var ClaudeDriver = class {
1245
1318
  "--runtime",
1246
1319
  this.id,
1247
1320
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1248
- "--deprecated-shim"
1321
+ "--runtime-actions-only"
1249
1322
  ]
1250
1323
  }
1251
1324
  }
@@ -1255,7 +1328,7 @@ var ClaudeDriver = class {
1255
1328
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1256
1329
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1257
1330
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1258
- writeFileSync2(mcpConfigPath, this.buildDeprecatedShimMcpConfig(ctx), { mode: 384 });
1331
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1259
1332
  return { systemPromptPath, mcpConfigPath };
1260
1333
  }
1261
1334
  spawn(ctx) {
@@ -1264,7 +1337,7 @@ var ClaudeDriver = class {
1264
1337
  const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt, {
1265
1338
  standingPromptFilePath: systemPromptPath
1266
1339
  });
1267
- args.push("--mcp-config", mcpConfigPath);
1340
+ args.push("--mcp-config", mcpConfigPath, "--strict-mcp-config");
1268
1341
  delete spawnEnv.CLAUDECODE;
1269
1342
  logger.info(
1270
1343
  `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
@@ -1388,7 +1461,9 @@ var ClaudeDriver = class {
1388
1461
  buildSystemPrompt(config, _agentId) {
1389
1462
  return buildCliTransportSystemPrompt(config, {
1390
1463
  toolPrefix: "mcp__chat__",
1391
- extraCriticalRules: [],
1464
+ extraCriticalRules: [
1465
+ "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `mcp__chat__runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
1466
+ ],
1392
1467
  postStartupNotes: [
1393
1468
  "**Claude runtime note:** Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.",
1394
1469
  "For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
@@ -1498,7 +1573,7 @@ var CodexDriver = class {
1498
1573
  probe() {
1499
1574
  return probeCodex();
1500
1575
  }
1501
- buildDeprecatedShimConfigArgs(ctx) {
1576
+ buildRuntimeActionsConfigArgs(ctx) {
1502
1577
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1503
1578
  const command = isTsSource ? "npx" : "node";
1504
1579
  const bridgeArgs = isTsSource ? [
@@ -1513,7 +1588,7 @@ var CodexDriver = class {
1513
1588
  "--runtime",
1514
1589
  this.id,
1515
1590
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1516
- "--deprecated-shim"
1591
+ "--runtime-actions-only"
1517
1592
  ] : [
1518
1593
  ctx.chatBridgePath,
1519
1594
  "--agent-id",
@@ -1525,7 +1600,7 @@ var CodexDriver = class {
1525
1600
  "--runtime",
1526
1601
  this.id,
1527
1602
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1528
- "--deprecated-shim"
1603
+ "--runtime-actions-only"
1529
1604
  ];
1530
1605
  return [
1531
1606
  "-c",
@@ -1595,7 +1670,7 @@ var CodexDriver = class {
1595
1670
  this.streamedAgentMessageIds.clear();
1596
1671
  this.streamedReasoningIds.clear();
1597
1672
  const args = ["app-server", "--listen", "stdio://"];
1598
- args.push(...this.buildDeprecatedShimConfigArgs(ctx));
1673
+ args.push(...this.buildRuntimeActionsConfigArgs(ctx));
1599
1674
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
1600
1675
  const proc = spawn2(command, spawnArgs, {
1601
1676
  cwd: ctx.workingDirectory,
@@ -2660,6 +2735,86 @@ function formatMessageTarget(message) {
2660
2735
  function getMessageShortId(messageId) {
2661
2736
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
2662
2737
  }
2738
+ function findSessionJsonl(root, predicate) {
2739
+ let visited = 0;
2740
+ const maxEntries = 1e4;
2741
+ const maxDepth = 8;
2742
+ const visit = (dir, depth) => {
2743
+ if (depth < 0 || visited >= maxEntries) return null;
2744
+ let entries;
2745
+ try {
2746
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
2747
+ } catch {
2748
+ return null;
2749
+ }
2750
+ for (const entry of entries) {
2751
+ if (++visited > maxEntries) return null;
2752
+ if (!entry.isFile() || !predicate(entry.name)) continue;
2753
+ return path10.join(dir, entry.name);
2754
+ }
2755
+ for (const entry of entries) {
2756
+ if (++visited > maxEntries) return null;
2757
+ if (!entry.isDirectory()) continue;
2758
+ const found = visit(path10.join(dir, entry.name), depth - 1);
2759
+ if (found) return found;
2760
+ }
2761
+ return null;
2762
+ };
2763
+ return visit(root, maxDepth);
2764
+ }
2765
+ function safeSessionFilename(value) {
2766
+ const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
2767
+ return normalized || "unknown-session";
2768
+ }
2769
+ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
2770
+ try {
2771
+ const dir = path10.join(fallbackDir, ".slock", "runtime-sessions");
2772
+ mkdirSync4(dir, { recursive: true });
2773
+ const filePath = path10.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
2774
+ writeFileSync7(filePath, JSON.stringify({
2775
+ type: "runtime_session_handoff",
2776
+ runtime,
2777
+ sessionId,
2778
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2779
+ note: "The native runtime transcript file was not found on this machine; this daemon-created handoff records the previous session identity for Runtime Profile migration."
2780
+ }) + "\n", { mode: 384 });
2781
+ return {
2782
+ label: sessionId,
2783
+ path: filePath,
2784
+ runtime,
2785
+ reachable: true,
2786
+ reason: "native session file path not found; using daemon handoff file"
2787
+ };
2788
+ } catch {
2789
+ return null;
2790
+ }
2791
+ }
2792
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), fallbackDir) {
2793
+ const directPath = path10.isAbsolute(sessionId) ? sessionId : null;
2794
+ if (directPath) {
2795
+ try {
2796
+ if (statSync(directPath).isFile()) {
2797
+ return { label: sessionId, path: directPath, runtime, reachable: true };
2798
+ }
2799
+ } catch {
2800
+ }
2801
+ }
2802
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path10.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path10.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
2803
+ if (!resolvedPath && fallbackDir) {
2804
+ const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
2805
+ if (fallback) return fallback;
2806
+ }
2807
+ const ref = {
2808
+ label: sessionId,
2809
+ path: resolvedPath ?? sessionId,
2810
+ runtime,
2811
+ reachable: Boolean(resolvedPath)
2812
+ };
2813
+ if (!resolvedPath) {
2814
+ ref.reason = "session file path not found";
2815
+ }
2816
+ return ref;
2817
+ }
2663
2818
  function formatSenderHandle(message) {
2664
2819
  return message.sender_description ? `@${message.sender_name} \u2014 ${message.sender_description}` : `@${message.sender_name}`;
2665
2820
  }
@@ -2699,6 +2854,42 @@ function formatIncomingMessage(message) {
2699
2854
  return threadJoinPrefix ? `${threadJoinPrefix}
2700
2855
  ${body}` : body;
2701
2856
  }
2857
+ function formatRuntimeProfileControlPrompt(messages) {
2858
+ const controls = messages.map((message) => ({
2859
+ message,
2860
+ notification: runtimeProfileNotificationFromMessage(message)
2861
+ }));
2862
+ if (controls.length === 0 || controls.some(({ notification }) => !notification)) {
2863
+ return null;
2864
+ }
2865
+ const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
2866
+ return [
2867
+ "Runtime Profile control notice.",
2868
+ "",
2869
+ "Complete the required runtime control action before reading or responding to normal inbox messages.",
2870
+ "",
2871
+ body,
2872
+ "",
2873
+ "Do not answer this notice in prose as the acknowledgment."
2874
+ ].join("\n");
2875
+ }
2876
+ function formatRuntimeProfileControlStartupInput(control, driver) {
2877
+ if (control.kind !== "migration") {
2878
+ return [
2879
+ "Read the Runtime Profile daemon release notice from your system prompt before normal work.",
2880
+ "No chat reply is required for this notice. Stop after reading it; queued inbox messages will be delivered separately."
2881
+ ].join("\n");
2882
+ }
2883
+ const actionName = `${driver.mcpToolPrefix}runtime_profile_migration_done`;
2884
+ return [
2885
+ "Runtime Profile migration is required before normal work.",
2886
+ `Invoke the available runtime control action \`${actionName}\` with exactly this migration_key: \`${control.key}\`.`,
2887
+ "Do not read MEMORY.md, check messages, or send a chat reply before this tool call.",
2888
+ "After the runtime control action succeeds, stop. Queued inbox messages will be delivered separately.",
2889
+ "",
2890
+ control.message
2891
+ ].join("\n");
2892
+ }
2702
2893
  function buildUnreadSummary(messages, excludeChannel) {
2703
2894
  const summary = /* @__PURE__ */ new Map();
2704
2895
  for (const message of messages) {
@@ -2723,6 +2914,16 @@ var FIRST_CINDY_SEED_MODE = "first-cindy";
2723
2914
  function getOnboardingSeedMode(config) {
2724
2915
  return (config.envVars?.[ONBOARDING_MEMORY_SEED_ENV] || "").trim().toLowerCase();
2725
2916
  }
2917
+ function withLocalRuntimeContext(config, agentId, workspacePath) {
2918
+ return {
2919
+ ...config,
2920
+ runtimeContext: {
2921
+ ...config.runtimeContext ?? {},
2922
+ agentId: config.runtimeContext?.agentId ?? agentId,
2923
+ workspacePath
2924
+ }
2925
+ };
2926
+ }
2726
2927
  function buildOnboardingPlaybookMd() {
2727
2928
  return `# Cindy Onboarding Playbook
2728
2929
 
@@ -2731,14 +2932,18 @@ Start warm and brief.
2731
2932
  Move quickly to one useful action, not a feature tour.
2732
2933
  Keep activation energy low: invite the user to start with one sentence about what they need now.
2733
2934
 
2734
- ## Step 2: Capture Minimal Context
2735
- Ask only what is needed to route:
2736
- 1. What is your role?
2737
- 2. What are you working on these days?
2935
+ ## Step 2: Activate or Propose
2936
+ Use one decision: does the user already know what they want to do?
2937
+ - Yes: skip role/work intake and propose a starter plan.
2938
+ - No: ask what they do and what they are working on. These questions are activation, not a questionnaire.
2939
+
2940
+ After any usable signal, stop asking and propose.
2941
+ After confirming language preference, do not give a generic product introduction; move into the user's work or a starter action.
2738
2942
 
2739
2943
  ## Step 3: Route by Intent (A-E)
2740
2944
  - A: Specific project/task
2741
- - Map immediately to first setup actions (agents + channels + first task).
2945
+ - Enter starter-task mode immediately.
2946
+ - Propose first setup actions before asking for more detail.
2742
2947
  - B: "What can you do?" curiosity
2743
2948
  - Proactively share 1-2 interview-grounded examples, then ask the user to pick one.
2744
2949
  - Use this opener tone: "Here are some examples our users have shared with us. I'm sharing these to inspire you."
@@ -2749,6 +2954,28 @@ Ask only what is needed to route:
2749
2954
  - E: Low-intent greeting/testing
2750
2955
  - Use a low-pressure prompt and guide to one concrete starter action.
2751
2956
 
2957
+ ### Starter Plan Output
2958
+ A starter plan should make the next action executable, not just descriptive:
2959
+ - agent name + role description that can be copied into the create-agent form
2960
+ - suggested channel or workstream
2961
+ - first task to send after creation
2962
+ - next UI action if the user needs to create an agent or channel
2963
+
2964
+ Do not use a rigid keyword routing table. Use examples as inspiration, then adapt to the user's context.
2965
+ If details are missing but not blocking, state reasonable defaults and invite correction.
2966
+ Only ask one blocking question first if the answer is required before any useful starter plan can be proposed.
2967
+ Do not imply you have already created agents or channels unless the action has actually happened.
2968
+
2969
+ ### Capability Boundary Pivot
2970
+ If the user's primary request is outside current capabilities, acknowledge the limitation once and pivot immediately to the nearest useful alternative.
2971
+ Do not repeat that something is impossible across multiple turns.
2972
+ Offer a concrete substitute: a manual input path, a narrower analysis task, an agent/team setup, or another workflow Slock can execute now.
2973
+
2974
+ ### Active-Elsewhere Handoff
2975
+ Channel silence is not failure.
2976
+ If the user is already active outside the onboarding channel, follow the work instead of trying to pull them back.
2977
+ Offer a concrete next step in the context they are using: first task, second agent suggestion, channel structure, or reminder.
2978
+
2752
2979
  ## Step 4: Progress Setup (Soft Guidance)
2753
2980
  While helping with real work, progressively shape:
2754
2981
  - initial team target >= 3 agents
@@ -2762,6 +2989,8 @@ Do not force setup before value.
2762
2989
 
2763
2990
  ## Step 5: End Every Turn with One Next Step
2764
2991
  Each reply should end with one clear, immediate action.
2992
+ At wrap-up, if there is a concrete next check-in, ask consent to set one contextual reminder.
2993
+ The reminder must reference the user's goal, agent, recent step, or suggested next action; do not send generic "come back later" reminders.
2765
2994
 
2766
2995
  ## Inspiration Stories (Interview-Grounded)
2767
2996
  - Story 1: "Sense of abundance" \u2014 agents self-organize, you do not need to micro-manage.
@@ -2954,6 +3183,34 @@ Do not copy these answers verbatim.
2954
3183
 
2955
3184
  ### Guardrail
2956
3185
  - Keep contact guidance concrete and current; do not invent alternative support channels.
3186
+
3187
+ ## FAQ 14: Can I use Slock on my phone?
3188
+ ### Answer idea
3189
+ - Yes. Slock can be used from a mobile browser.
3190
+ - For easier return access, users can add Slock to their phone home screen as a web app:
3191
+ - iPhone: Safari \u2192 Share \u2192 Add to Home Screen
3192
+ - Android: Chrome \u2192 menu \u2192 Add to Home Screen / Install app
3193
+ - Good mobile use cases: quick check-ins, todos/reminders, short replies, and reviewing agent updates.
3194
+
3195
+ ### Next step
3196
+ - If the user wants mobile access now, ask whether they use iPhone or Android, then guide the matching Add to Home Screen step.
3197
+
3198
+ ### Guardrail
3199
+ - Do not imply Slock has a native iOS/Android App Store app.
3200
+ - Do not over-sell it as fully equivalent to a native app; call it mobile browser / home-screen web app.
3201
+
3202
+ ## FAQ 15: How do I create agents or channels?
3203
+ ### Answer idea
3204
+ - The user can create agents from the Agents section in the People tab by clicking the + button, or from a computer/machine context menu via Create Agent.
3205
+ - The user can create channels from the Channels section by clicking the + button.
3206
+ - When recommending setup, provide copyable agent names/descriptions, suggested channel names, and the first task to send after creation.
3207
+
3208
+ ### Next step
3209
+ - Give the exact agent/channel spec the user can copy, then guide them to the relevant + button or creation dialog.
3210
+
3211
+ ### Guardrail
3212
+ - Do not imply you already created agents or channels unless that action actually happened.
3213
+ - If you cannot directly create something, avoid a long permissions explanation; give the copyable spec and the next UI action.
2957
3214
  `;
2958
3215
  }
2959
3216
  function buildOnboardingSeedFiles() {
@@ -3129,7 +3386,12 @@ function getBusyDeliveryNote(driver) {
3129
3386
  }
3130
3387
  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.";
3131
3388
  }
3132
- var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
3389
+ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = (
3390
+ // Claude Code 2.1.114 treats "follow your system prompt" style user turns as
3391
+ // prompt-injection bait even when the prompt is supplied via --system-prompt-file.
3392
+ // A neutral starter lets the native standing prompt drive startup/migration.
3393
+ "Start."
3394
+ );
3133
3395
  var AgentProcessManager = class _AgentProcessManager {
3134
3396
  agents = /* @__PURE__ */ new Map();
3135
3397
  agentsStarting = /* @__PURE__ */ new Set();
@@ -3143,6 +3405,7 @@ var AgentProcessManager = class _AgentProcessManager {
3143
3405
  daemonApiKey;
3144
3406
  serverUrl;
3145
3407
  dataDir;
3408
+ runtimeSessionHomeDir;
3146
3409
  driverResolver;
3147
3410
  defaultAgentEnvVarsProvider;
3148
3411
  tracer;
@@ -3153,6 +3416,7 @@ var AgentProcessManager = class _AgentProcessManager {
3153
3416
  this.daemonApiKey = daemonApiKey;
3154
3417
  this.serverUrl = opts.serverUrl;
3155
3418
  this.dataDir = opts.dataDir || DATA_DIR;
3419
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os3.homedir();
3156
3420
  this.driverResolver = opts.driverResolver || getDriver;
3157
3421
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3158
3422
  this.tracer = opts.tracer ?? noopTracer;
@@ -3171,11 +3435,12 @@ var AgentProcessManager = class _AgentProcessManager {
3171
3435
  const driver = this.driverResolver(config.runtime || "claude");
3172
3436
  const agentDataDir = path10.join(this.dataDir, agentId);
3173
3437
  await mkdir(agentDataDir, { recursive: true });
3438
+ const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
3174
3439
  const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
3175
3440
  try {
3176
3441
  await access(memoryMdPath);
3177
3442
  } catch {
3178
- const initialMemoryMd = buildInitialMemoryMd(config);
3443
+ const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
3179
3444
  await writeFile(memoryMdPath, initialMemoryMd);
3180
3445
  }
3181
3446
  const notesDir = path10.join(agentDataDir, "notes");
@@ -3192,18 +3457,21 @@ var AgentProcessManager = class _AgentProcessManager {
3192
3457
  }
3193
3458
  }
3194
3459
  }
3195
- const isResume = !!config.sessionId;
3196
- const standingPrompt = driver.buildSystemPrompt(config, agentId);
3460
+ const isResume = !!runtimeConfig.sessionId;
3461
+ const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
3197
3462
  let prompt;
3198
- if (isResume && resumePrompt) {
3463
+ if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
3464
+ prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
3465
+ } else if (isResume && resumePrompt) {
3199
3466
  prompt = resumePrompt;
3200
3467
  prompt += getBusyDeliveryNote(driver);
3201
3468
  } else if (wakeMessage) {
3469
+ const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
3202
3470
  const channelLabel = formatChannelLabel(wakeMessage);
3203
- prompt = `New message received:
3471
+ prompt = runtimeProfileControlPrompt ?? `New message received:
3204
3472
 
3205
3473
  ${formatIncomingMessage(wakeMessage)}`;
3206
- if (unreadSummary && Object.keys(unreadSummary).length > 0) {
3474
+ if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
3207
3475
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
3208
3476
  if (otherUnread.length > 0) {
3209
3477
  prompt += `
@@ -3218,12 +3486,14 @@ You also have unread messages in other channels:`;
3218
3486
  Use read_history to catch up, or respond to the message above first.`;
3219
3487
  }
3220
3488
  }
3221
- prompt += `
3489
+ if (!runtimeProfileControlPrompt) {
3490
+ prompt += `
3222
3491
 
3223
3492
  Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
3224
3493
 
3225
3494
  IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
3226
- prompt += getBusyDeliveryNote(driver);
3495
+ prompt += getBusyDeliveryNote(driver);
3496
+ }
3227
3497
  } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
3228
3498
  prompt = `You have unread messages from while you were offline:`;
3229
3499
  for (const [ch, count] of Object.entries(unreadSummary)) {
@@ -3239,7 +3509,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3239
3509
  } else {
3240
3510
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
3241
3511
  }
3242
- const effectiveConfig = await this.buildSpawnConfig(agentId, config);
3512
+ const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
3243
3513
  const { process: proc } = driver.spawn({
3244
3514
  agentId,
3245
3515
  config: effectiveConfig,
@@ -3255,8 +3525,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3255
3525
  process: proc,
3256
3526
  driver,
3257
3527
  inbox: this.startingInboxes.get(agentId) || [],
3258
- config,
3259
- sessionId: config.sessionId || null,
3528
+ config: runtimeConfig,
3529
+ sessionId: runtimeConfig.sessionId || null,
3260
3530
  launchId: launchId || null,
3261
3531
  isIdle: false,
3262
3532
  notificationTimer: null,
@@ -3282,6 +3552,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3282
3552
  this.agents.set(agentId, agentProcess);
3283
3553
  this.startRuntimeTrace(agentId, agentProcess, "spawn");
3284
3554
  this.agentsStarting.delete(agentId);
3555
+ if (config.runtimeProfileControl) {
3556
+ this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
3557
+ }
3285
3558
  if (wakeMessage) {
3286
3559
  this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
3287
3560
  }
@@ -3492,6 +3765,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3492
3765
  this.agents.delete(agentId);
3493
3766
  ap.process.kill("SIGTERM");
3494
3767
  if (!silent) {
3768
+ this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
3495
3769
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
3496
3770
  this.broadcastActivity(agentId, "offline", "Stopped");
3497
3771
  logger.info(`[Agent ${agentId}] Stopped by request`);
@@ -3610,12 +3884,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3610
3884
  path: workspacePath,
3611
3885
  reachable: true
3612
3886
  },
3613
- sessionRef: sessionId ? {
3614
- label: sessionId,
3615
- path: sessionId,
3616
- runtime: config.runtime,
3617
- reachable: true
3618
- } : null
3887
+ sessionRef: sessionId ? resolveRuntimeSessionRef(config.runtime, sessionId, this.runtimeSessionHomeDir, workspacePath) : null
3619
3888
  }
3620
3889
  };
3621
3890
  }
@@ -3706,9 +3975,26 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3706
3975
  }
3707
3976
  }
3708
3977
  }
3709
- sendRuntimeProfileReport(agentId) {
3710
- const report = this.getAgentRuntimeProfileReport(agentId);
3711
- if (!report) return;
3978
+ ackInjectedRuntimeProfileControl(agentId, control, launchId) {
3979
+ const title = runtimeProfileNotificationTitle(control.kind);
3980
+ this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
3981
+ if (control.kind === "migration") {
3982
+ this.sendToServer({
3983
+ type: "agent:runtime_profile:migration:ack",
3984
+ agentId,
3985
+ migrationKey: control.key,
3986
+ launchId: launchId || void 0
3987
+ });
3988
+ } else {
3989
+ this.sendToServer({
3990
+ type: "agent:runtime_profile:daemon_release_notice:ack",
3991
+ agentId,
3992
+ noticeKey: control.key,
3993
+ launchId: launchId || void 0
3994
+ });
3995
+ }
3996
+ }
3997
+ sendRuntimeProfileWireReport(report) {
3712
3998
  this.sendToServer({
3713
3999
  type: "agent:runtime_profile",
3714
4000
  agentId: report.agentId,
@@ -3716,6 +4002,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3716
4002
  launchId: report.launchId || void 0
3717
4003
  });
3718
4004
  }
4005
+ sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4006
+ this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
4007
+ }
4008
+ sendRuntimeProfileReport(agentId) {
4009
+ const report = this.getAgentRuntimeProfileReport(agentId);
4010
+ if (!report) return;
4011
+ this.sendRuntimeProfileWireReport(report);
4012
+ }
3719
4013
  // Machine-level workspace scanning
3720
4014
  async scanAllWorkspaces() {
3721
4015
  return scanWorkspaceDirectories(this.dataDir);
@@ -4311,7 +4605,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4311
4605
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
4312
4606
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
4313
4607
  if (messages.length === 0) return true;
4314
- const prompt = messages.length === 1 ? `New message received:
4608
+ const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
4315
4609
 
4316
4610
  ${formatIncomingMessage(messages[0])}
4317
4611
 
@@ -4319,7 +4613,7 @@ Respond as appropriate. Complete all your work before stopping.` : `New messages
4319
4613
 
4320
4614
  ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
4321
4615
 
4322
- Respond as appropriate. Complete all your work before stopping.`;
4616
+ Respond as appropriate. Complete all your work before stopping.`);
4323
4617
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
4324
4618
  if (!encoded) {
4325
4619
  ap.inbox.unshift(...messages);
@@ -4585,6 +4879,112 @@ var ReminderCache = class {
4585
4879
  }
4586
4880
  };
4587
4881
 
4882
+ // src/machineLock.ts
4883
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
4884
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
4885
+ import os4 from "os";
4886
+ import path11 from "path";
4887
+ var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
4888
+ var INCOMPLETE_LOCK_STALE_MS = 3e4;
4889
+ var DaemonMachineLockConflictError = class extends Error {
4890
+ code = "DAEMON_MACHINE_LOCK_HELD";
4891
+ constructor(lockDir, owner) {
4892
+ const ownerText = owner ? `pid=${owner.pid}, startedAt=${owner.startedAt}, host=${owner.hostname}` : "unknown owner";
4893
+ super(
4894
+ `Another Slock daemon is already running for this machine key (${ownerText}). Lock: ${lockDir}. Stop the existing daemon first, or use a different machine key/state directory.`
4895
+ );
4896
+ this.name = "DaemonMachineLockConflictError";
4897
+ }
4898
+ };
4899
+ function apiKeyFingerprint(apiKey) {
4900
+ return createHash("sha256").update(apiKey).digest("hex");
4901
+ }
4902
+ function getDaemonMachineLockId(apiKey) {
4903
+ return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
4904
+ }
4905
+ function ownerPath(lockDir) {
4906
+ return path11.join(lockDir, "owner.json");
4907
+ }
4908
+ function readOwner(lockDir) {
4909
+ try {
4910
+ return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
4911
+ } catch {
4912
+ return null;
4913
+ }
4914
+ }
4915
+ function lockAgeMs(lockDir) {
4916
+ try {
4917
+ return Date.now() - statSync2(lockDir).mtimeMs;
4918
+ } catch {
4919
+ return null;
4920
+ }
4921
+ }
4922
+ function isProcessAlive(pid) {
4923
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4924
+ try {
4925
+ process.kill(pid, 0);
4926
+ return true;
4927
+ } catch (err) {
4928
+ const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
4929
+ return code !== "ESRCH";
4930
+ }
4931
+ }
4932
+ function acquireDaemonMachineLock(options) {
4933
+ const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
4934
+ const fingerprint = apiKeyFingerprint(options.apiKey);
4935
+ const lockId = getDaemonMachineLockId(options.apiKey);
4936
+ const machineDir = path11.join(rootDir, lockId);
4937
+ const lockDir = path11.join(machineDir, "daemon.lock");
4938
+ const token = randomUUID2();
4939
+ mkdirSync5(machineDir, { recursive: true });
4940
+ for (let attempt = 0; attempt < 2; attempt += 1) {
4941
+ try {
4942
+ mkdirSync5(lockDir);
4943
+ const owner = {
4944
+ pid: process.pid,
4945
+ token,
4946
+ hostname: os4.hostname(),
4947
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4948
+ serverUrl: options.serverUrl,
4949
+ apiKeyFingerprint: fingerprint.slice(0, 16)
4950
+ };
4951
+ try {
4952
+ writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
4953
+ `, { mode: 384 });
4954
+ } catch (err) {
4955
+ rmSync2(lockDir, { recursive: true, force: true });
4956
+ throw err;
4957
+ }
4958
+ return {
4959
+ lockId,
4960
+ machineDir,
4961
+ lockDir,
4962
+ release: () => {
4963
+ const currentOwner = readOwner(lockDir);
4964
+ if (currentOwner?.pid === process.pid && currentOwner.token === token) {
4965
+ rmSync2(lockDir, { recursive: true, force: true });
4966
+ }
4967
+ }
4968
+ };
4969
+ } catch (err) {
4970
+ const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
4971
+ if (code !== "EEXIST") throw err;
4972
+ const owner = readOwner(lockDir);
4973
+ if (owner?.pid && isProcessAlive(owner.pid)) {
4974
+ throw new DaemonMachineLockConflictError(lockDir, owner);
4975
+ }
4976
+ if (!owner) {
4977
+ const ageMs = lockAgeMs(lockDir);
4978
+ if (ageMs === null || ageMs < INCOMPLETE_LOCK_STALE_MS) {
4979
+ throw new DaemonMachineLockConflictError(lockDir, null);
4980
+ }
4981
+ }
4982
+ rmSync2(lockDir, { recursive: true, force: true });
4983
+ }
4984
+ }
4985
+ throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
4986
+ }
4987
+
4588
4988
  // src/core.ts
4589
4989
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
4590
4990
  function parseDaemonCliArgs(args) {
@@ -4606,23 +5006,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
4606
5006
  }
4607
5007
  }
4608
5008
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
4609
- const dirname = path11.dirname(fileURLToPath(moduleUrl));
4610
- const jsPath = path11.resolve(dirname, "chat-bridge.js");
5009
+ const dirname = path12.dirname(fileURLToPath(moduleUrl));
5010
+ const jsPath = path12.resolve(dirname, "chat-bridge.js");
4611
5011
  try {
4612
5012
  accessSync(jsPath);
4613
5013
  return jsPath;
4614
5014
  } catch {
4615
- return path11.resolve(dirname, "chat-bridge.ts");
5015
+ return path12.resolve(dirname, "chat-bridge.ts");
4616
5016
  }
4617
5017
  }
4618
5018
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
4619
- const thisDir = path11.dirname(fileURLToPath(moduleUrl));
4620
- const bundledDistPath = path11.resolve(thisDir, "cli", "index.js");
5019
+ const thisDir = path12.dirname(fileURLToPath(moduleUrl));
5020
+ const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
4621
5021
  try {
4622
5022
  accessSync(bundledDistPath);
4623
5023
  return bundledDistPath;
4624
5024
  } catch {
4625
- const workspaceDistPath = path11.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5025
+ const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
4626
5026
  accessSync(workspaceDistPath);
4627
5027
  return workspaceDistPath;
4628
5028
  }
@@ -4701,6 +5101,7 @@ var DaemonCore = class {
4701
5101
  connection;
4702
5102
  reminderCache;
4703
5103
  tracer;
5104
+ machineLock = null;
4704
5105
  constructor(options) {
4705
5106
  this.options = options;
4706
5107
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
@@ -4731,15 +5132,39 @@ var DaemonCore = class {
4731
5132
  });
4732
5133
  this.connection = connection;
4733
5134
  }
5135
+ resolveMachineStateRoot() {
5136
+ if (this.options.machineStateDir) return this.options.machineStateDir;
5137
+ if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
5138
+ return DEFAULT_MACHINE_STATE_ROOT;
5139
+ }
4734
5140
  start() {
4735
5141
  logger.info("[Slock Daemon] Starting...");
4736
- this.connection.connect();
5142
+ if (!this.machineLock) {
5143
+ this.machineLock = acquireDaemonMachineLock({
5144
+ apiKey: this.options.apiKey,
5145
+ serverUrl: this.options.serverUrl,
5146
+ rootDir: this.resolveMachineStateRoot()
5147
+ });
5148
+ logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
5149
+ }
5150
+ try {
5151
+ this.connection.connect();
5152
+ } catch (err) {
5153
+ this.machineLock.release();
5154
+ this.machineLock = null;
5155
+ throw err;
5156
+ }
4737
5157
  }
4738
5158
  async stop() {
4739
5159
  logger.info("[Slock Daemon] Shutting down...");
4740
5160
  this.reminderCache.clear();
4741
- await this.agentManager.stopAll();
4742
- this.connection.disconnect();
5161
+ try {
5162
+ await this.agentManager.stopAll();
5163
+ } finally {
5164
+ this.connection.disconnect();
5165
+ this.machineLock?.release();
5166
+ this.machineLock = null;
5167
+ }
4743
5168
  }
4744
5169
  get connected() {
4745
5170
  return this.connection.connected;
@@ -4900,8 +5325,8 @@ var DaemonCore = class {
4900
5325
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
4901
5326
  runtimes,
4902
5327
  runningAgents: this.agentManager.getRunningAgentIds(),
4903
- hostname: this.options.hostname ?? os4.hostname(),
4904
- os: this.options.osDescription ?? `${os4.platform()} ${os4.arch()}`,
5328
+ hostname: this.options.hostname ?? os5.hostname(),
5329
+ os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
4905
5330
  daemonVersion: this.daemonVersion
4906
5331
  });
4907
5332
  for (const agentId of this.agentManager.getRunningAgentIds()) {