@slock-ai/daemon 0.41.1-alpha.0 → 0.43.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 path12 from "path";
8
- import os5 from "os";
7
+ import path13 from "path";
8
+ import os6 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
11
11
  import { fileURLToPath } from "url";
@@ -545,8 +545,57 @@ var RUNTIMES = [
545
545
  { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
546
546
  { id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
547
547
  { id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
548
- { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
548
+ { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
549
+ { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
549
550
  ];
551
+ var RUNTIME_MODELS = {
552
+ claude: [
553
+ { id: "sonnet", label: "Sonnet" },
554
+ { id: "opus", label: "Opus" },
555
+ { id: "haiku", label: "Haiku" }
556
+ ],
557
+ codex: [
558
+ { id: "gpt-5.5", label: "GPT-5.5" },
559
+ { id: "gpt-5.4", label: "GPT-5.4" },
560
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
561
+ { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
562
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
563
+ { id: "gpt-5.2", label: "GPT-5.2" },
564
+ { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
565
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
566
+ { id: "gpt-5-codex", label: "GPT-5 Codex" },
567
+ { id: "gpt-5", label: "GPT-5" }
568
+ ],
569
+ copilot: [
570
+ { id: "gpt-5.4", label: "GPT-5.4" },
571
+ { id: "gpt-5.2", label: "GPT-5.2" },
572
+ { id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
573
+ { id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
574
+ ],
575
+ cursor: [
576
+ { id: "composer-2-fast", label: "Composer 2 Fast" },
577
+ { id: "composer-2", label: "Composer 2" },
578
+ { id: "auto", label: "Auto" }
579
+ ],
580
+ gemini: [
581
+ { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
582
+ { id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
583
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
584
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
585
+ ],
586
+ opencode: [
587
+ { id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
588
+ { id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
589
+ { id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
590
+ { id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
591
+ { id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
592
+ ],
593
+ // Kimi CLI resolves model keys from each user's local config, so the safest
594
+ // built-in option is to defer to whatever default model the CLI already uses.
595
+ kimi: [
596
+ { id: "default", label: "Configured Default" }
597
+ ]
598
+ };
550
599
  var PLAN_CONFIG = {
551
600
  free: {
552
601
  displayName: "Hobby",
@@ -582,9 +631,10 @@ var DISPLAY_PLAN_CONFIG = {
582
631
  };
583
632
 
584
633
  // src/agentProcessManager.ts
634
+ import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
585
635
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
586
- import path10 from "path";
587
- import os3 from "os";
636
+ import path11 from "path";
637
+ import os4 from "os";
588
638
 
589
639
  // src/drivers/claude.ts
590
640
  import { spawn } from "child_process";
@@ -599,6 +649,27 @@ import path from "path";
599
649
  function toolRef(prefix, name) {
600
650
  return `${prefix}${name}`;
601
651
  }
652
+ function runtimeContextLines(config) {
653
+ const ctx = config.runtimeContext;
654
+ if (!ctx) return [];
655
+ const lines = [
656
+ "## Current Runtime Context",
657
+ "",
658
+ "This is authoritative context injected by Slock. Do not infer computer identity from hostname or cwd when this section is present.",
659
+ ""
660
+ ];
661
+ if (ctx.agentId) lines.push(`- Agent ID: ${ctx.agentId}`);
662
+ if (ctx.serverId) lines.push(`- Server ID: ${ctx.serverId}`);
663
+ if (ctx.machineName || ctx.machineId) {
664
+ const label = ctx.machineName && ctx.machineId ? `${ctx.machineName} (${ctx.machineId})` : ctx.machineName || ctx.machineId;
665
+ lines.push(`- Computer: ${label}`);
666
+ }
667
+ if (ctx.machineHostname) lines.push(`- Hostname: ${ctx.machineHostname}`);
668
+ if (ctx.machineOs) lines.push(`- OS: ${ctx.machineOs}`);
669
+ if (ctx.daemonVersion) lines.push(`- Daemon: v${ctx.daemonVersion}`);
670
+ if (ctx.workspacePath) lines.push(`- Workspace: ${ctx.workspacePath}`);
671
+ return lines.length > 4 ? lines : [];
672
+ }
602
673
  function buildPrompt(config, variant, opts) {
603
674
  const isCli = variant === "cli";
604
675
  const t = (name) => toolRef(opts.toolPrefix, name);
@@ -609,6 +680,9 @@ function buildPrompt(config, variant, opts) {
609
680
  const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
610
681
  const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
611
682
  const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
683
+ const scheduleReminderCmd = isCli ? "`slock reminder schedule`" : `\`${t("schedule_reminder")}\``;
684
+ const listRemindersCmd = isCli ? "`slock reminder list`" : `\`${t("list_reminders")}\``;
685
+ const cancelReminderCmd = isCli ? "`slock reminder cancel`" : `\`${t("cancel_reminder")}\``;
612
686
  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.";
613
687
  const criticalRules = isCli ? [
614
688
  "- Always communicate through `slock` CLI commands. This is your only output channel.",
@@ -621,13 +695,18 @@ function buildPrompt(config, variant, opts) {
621
695
  "- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
622
696
  `- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
623
697
  ];
698
+ const runtimeProfileControlStartupStep = config.runtimeProfileControl ? [
699
+ "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."
700
+ ] : [];
624
701
  const startupSteps = isCli ? [
702
+ ...runtimeProfileControlStartupStep,
625
703
  "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
704
  "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
627
705
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
628
706
  "4. When you receive a message, process it and reply with `slock message send`.",
629
707
  "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
708
  ] : [
709
+ ...runtimeProfileControlStartupStep,
631
710
  `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
711
  "2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
633
712
  `3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
@@ -641,24 +720,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
641
720
  1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
642
721
  2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
643
722
  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.
658
-
659
- 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
- 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.
661
- For agent-created reminders, first resolve the anchor message from the current conversation and pass its \`msgId\` explicitly. If you cannot resolve a message id, do not create the reminder.
723
+ 4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
724
+ 5. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
725
+ 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.
726
+ 7. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
727
+ 8. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
728
+ 9. **\`slock task list\`** \u2014 View a channel's task board.
729
+ 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).
730
+ 11. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
731
+ 12. **\`slock task unclaim\`** \u2014 Release your claim on a task.
732
+ 13. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
733
+ 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\`.
734
+ 15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
735
+ 16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
736
+ 17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
737
+ 18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
738
+ 19. **\`slock reminder list\`** \u2014 List your reminders.
739
+ 20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
662
740
 
663
741
  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:
664
742
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -682,7 +760,15 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
682
760
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
683
761
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
684
762
  12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
685
- 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
763
+ 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
764
+ 14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
765
+ 15. **${listRemindersCmd}** \u2014 List your reminders.
766
+ 16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
767
+ const reminderSection = `### Reminders
768
+
769
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
770
+ Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
771
+ Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
686
772
  const sendingMessagesSection = isCli ? `### Sending messages
687
773
 
688
774
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
@@ -728,7 +814,7 @@ Threads are sub-conversations attached to a specific message. They let you discu
728
814
  const discoverySection = isCli ? `### Discovering people and channels
729
815
 
730
816
  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
817
+ 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
818
 
733
819
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
734
820
  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")}\`.`;
@@ -752,6 +838,11 @@ To jump directly to a specific hit with nearby context, use \`slock message read
752
838
  Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
753
839
 
754
840
  To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
841
+ const historicalReferenceSection = isCli ? `### Historical references
842
+
843
+ When a user refers to prior Slock discussion and the relevant context is not already available, first use \`slock message search\` and \`slock message read\` to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.` : `### Historical references
844
+
845
+ When a user refers to prior Slock discussion and the relevant context is not already available, first use \`${t("search_messages")}\` and ${readCmd} to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.`;
755
846
  const tasksSection = isCli ? `### Tasks
756
847
 
757
848
  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.
@@ -838,6 +929,8 @@ ${readCmd} shows messages in their current state. If a message was later convert
838
929
 
839
930
  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
931
 
932
+ ${runtimeContextLines(config).join("\n")}
933
+
841
934
  ${communicationSection}
842
935
 
843
936
  CRITICAL RULES:
@@ -851,6 +944,30 @@ ${startupSteps.join("\n")}`;
851
944
 
852
945
  ${opts.postStartupNotes.join("\n")}`;
853
946
  }
947
+ if (config.runtimeProfileControl) {
948
+ const control = config.runtimeProfileControl;
949
+ prompt += `
950
+
951
+ ## Runtime Profile Control
952
+
953
+ `;
954
+ 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.
955
+
956
+ `;
957
+ if (control.kind === "migration") {
958
+ 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}\`.
959
+
960
+ `;
961
+ 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.
962
+
963
+ `;
964
+ } else {
965
+ prompt += `Read the daemon release notice below before handling normal inbox messages. No chat reply is required for this notice.
966
+
967
+ `;
968
+ }
969
+ prompt += control.message;
970
+ }
854
971
  prompt += `
855
972
 
856
973
  ## Messaging
@@ -875,6 +992,8 @@ Header fields:
875
992
 
876
993
  ${sendingMessagesSection}
877
994
 
995
+ ${reminderSection}
996
+
878
997
  ${threadsSection}
879
998
 
880
999
  ${discoverySection}
@@ -883,6 +1002,8 @@ ${channelAwarenessSection}
883
1002
 
884
1003
  ${readingHistorySection}
885
1004
 
1005
+ ${historicalReferenceSection}
1006
+
886
1007
  ${tasksSection}
887
1008
 
888
1009
  ### Splitting tasks for parallel execution
@@ -940,7 +1061,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
940
1061
 
941
1062
  ## Workspace & Memory
942
1063
 
943
- Your working directory (cwd) is your **persistent workspace**. Everything you write here survives across sessions.
1064
+ Your working directory (cwd) is your **persistent, agent-owned workspace**; files you create here survive across sessions. Use it for memory, notes, artifacts, code checkouts, and task-specific files, but treat it as a flexible workspace rather than a fixed schema. Keep **MEMORY.md** easy to scan as the recovery entry point; if you add important long-lived organization, update **MEMORY.md** or a note index so future sessions can find it. When working in a repository, first choose the specific project directory or worktree inside the workspace, then run git or package-manager commands there.
944
1065
 
945
1066
  ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
946
1067
 
@@ -1045,6 +1166,20 @@ function buildMcpSystemPrompt(config, opts) {
1045
1166
 
1046
1167
  // src/drivers/cliTransport.ts
1047
1168
  var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1169
+ function runtimeContextEnv(config) {
1170
+ const ctx = config.runtimeContext;
1171
+ if (!ctx) return {};
1172
+ return {
1173
+ ...ctx.agentId ? { SLOCK_CURRENT_AGENT_ID: ctx.agentId } : {},
1174
+ ...ctx.serverId ? { SLOCK_CURRENT_SERVER_ID: ctx.serverId } : {},
1175
+ ...ctx.machineId ? { SLOCK_CURRENT_COMPUTER_ID: ctx.machineId } : {},
1176
+ ...ctx.machineName ? { SLOCK_CURRENT_COMPUTER_NAME: ctx.machineName } : {},
1177
+ ...ctx.machineHostname ? { SLOCK_CURRENT_COMPUTER_HOSTNAME: ctx.machineHostname } : {},
1178
+ ...ctx.machineOs ? { SLOCK_CURRENT_COMPUTER_OS: ctx.machineOs } : {},
1179
+ ...ctx.daemonVersion ? { SLOCK_CURRENT_DAEMON_VERSION: ctx.daemonVersion } : {},
1180
+ ...ctx.workspacePath ? { SLOCK_CURRENT_WORKSPACE_PATH: ctx.workspacePath } : {}
1181
+ };
1182
+ }
1048
1183
  function buildCliTransportSystemPrompt(config, opts) {
1049
1184
  return buildCliSystemPrompt(config, opts);
1050
1185
  }
@@ -1074,6 +1209,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1074
1209
  FORCE_COLOR: "0",
1075
1210
  ...ctx.config.envVars || {},
1076
1211
  ...extraEnv,
1212
+ ...runtimeContextEnv(ctx.config),
1077
1213
  SLOCK_AGENT_ID: ctx.agentId,
1078
1214
  ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
1079
1215
  SLOCK_SERVER_URL: ctx.config.serverUrl,
@@ -1207,6 +1343,8 @@ var ClaudeDriver = class {
1207
1343
  "--allow-dangerously-skip-permissions",
1208
1344
  "--dangerously-skip-permissions",
1209
1345
  "--verbose",
1346
+ "--permission-mode",
1347
+ "bypassPermissions",
1210
1348
  "--output-format",
1211
1349
  "stream-json",
1212
1350
  "--input-format",
@@ -1217,16 +1355,16 @@ var ClaudeDriver = class {
1217
1355
  CLAUDE_DISALLOWED_TOOLS
1218
1356
  ];
1219
1357
  if (opts.standingPromptFilePath) {
1220
- args.push("--append-system-prompt-file", opts.standingPromptFilePath);
1358
+ args.push("--system-prompt-file", opts.standingPromptFilePath);
1221
1359
  } else {
1222
- args.push("--append-system-prompt", standingPrompt);
1360
+ args.push("--system-prompt", standingPrompt);
1223
1361
  }
1224
1362
  if (config.sessionId) {
1225
1363
  args.push("--resume", config.sessionId);
1226
1364
  }
1227
1365
  return args;
1228
1366
  }
1229
- buildDeprecatedShimMcpConfig(ctx) {
1367
+ buildRuntimeActionsMcpConfig(ctx) {
1230
1368
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1231
1369
  const command = isTsSource ? "npx" : "node";
1232
1370
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
@@ -1245,7 +1383,7 @@ var ClaudeDriver = class {
1245
1383
  "--runtime",
1246
1384
  this.id,
1247
1385
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1248
- "--deprecated-shim"
1386
+ "--runtime-actions-only"
1249
1387
  ]
1250
1388
  }
1251
1389
  }
@@ -1255,7 +1393,7 @@ var ClaudeDriver = class {
1255
1393
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1256
1394
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1257
1395
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1258
- writeFileSync2(mcpConfigPath, this.buildDeprecatedShimMcpConfig(ctx), { mode: 384 });
1396
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1259
1397
  return { systemPromptPath, mcpConfigPath };
1260
1398
  }
1261
1399
  spawn(ctx) {
@@ -1264,7 +1402,7 @@ var ClaudeDriver = class {
1264
1402
  const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt, {
1265
1403
  standingPromptFilePath: systemPromptPath
1266
1404
  });
1267
- args.push("--mcp-config", mcpConfigPath);
1405
+ args.push("--mcp-config", mcpConfigPath, "--strict-mcp-config");
1268
1406
  delete spawnEnv.CLAUDECODE;
1269
1407
  logger.info(
1270
1408
  `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
@@ -1388,7 +1526,9 @@ var ClaudeDriver = class {
1388
1526
  buildSystemPrompt(config, _agentId) {
1389
1527
  return buildCliTransportSystemPrompt(config, {
1390
1528
  toolPrefix: "mcp__chat__",
1391
- extraCriticalRules: [],
1529
+ extraCriticalRules: [
1530
+ "- 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."
1531
+ ],
1392
1532
  postStartupNotes: [
1393
1533
  "**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
1534
  "For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
@@ -1498,7 +1638,7 @@ var CodexDriver = class {
1498
1638
  probe() {
1499
1639
  return probeCodex();
1500
1640
  }
1501
- buildDeprecatedShimConfigArgs(ctx) {
1641
+ buildRuntimeActionsConfigArgs(ctx) {
1502
1642
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1503
1643
  const command = isTsSource ? "npx" : "node";
1504
1644
  const bridgeArgs = isTsSource ? [
@@ -1513,7 +1653,7 @@ var CodexDriver = class {
1513
1653
  "--runtime",
1514
1654
  this.id,
1515
1655
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1516
- "--deprecated-shim"
1656
+ "--runtime-actions-only"
1517
1657
  ] : [
1518
1658
  ctx.chatBridgePath,
1519
1659
  "--agent-id",
@@ -1525,7 +1665,7 @@ var CodexDriver = class {
1525
1665
  "--runtime",
1526
1666
  this.id,
1527
1667
  ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1528
- "--deprecated-shim"
1668
+ "--runtime-actions-only"
1529
1669
  ];
1530
1670
  return [
1531
1671
  "-c",
@@ -1595,7 +1735,7 @@ var CodexDriver = class {
1595
1735
  this.streamedAgentMessageIds.clear();
1596
1736
  this.streamedReasoningIds.clear();
1597
1737
  const args = ["app-server", "--listen", "stdio://"];
1598
- args.push(...this.buildDeprecatedShimConfigArgs(ctx));
1738
+ args.push(...this.buildRuntimeActionsConfigArgs(ctx));
1599
1739
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
1600
1740
  const proc = spawn2(command, spawnArgs, {
1601
1741
  cwd: ctx.workingDirectory,
@@ -2165,6 +2305,17 @@ var CursorDriver = class {
2165
2305
  import { spawn as spawn5 } from "child_process";
2166
2306
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2167
2307
  import path7 from "path";
2308
+ function buildGeminiSpawnEnv(ctx) {
2309
+ return {
2310
+ ...process.env,
2311
+ FORCE_COLOR: "0",
2312
+ NO_COLOR: "1",
2313
+ // Gemini CLI's trusted-workspace gate breaks our managed headless flow
2314
+ // unless we explicitly trust the daemon-owned agent workspace.
2315
+ GEMINI_CLI_TRUST_WORKSPACE: "true",
2316
+ ...ctx.config.envVars || {}
2317
+ };
2318
+ }
2168
2319
  var GeminiDriver = class {
2169
2320
  id = "gemini";
2170
2321
  supportsStdinNotification = false;
@@ -2204,7 +2355,7 @@ var GeminiDriver = class {
2204
2355
  if (ctx.config.sessionId) {
2205
2356
  args.push("--resume", ctx.config.sessionId);
2206
2357
  }
2207
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2358
+ const spawnEnv = buildGeminiSpawnEnv(ctx);
2208
2359
  const proc = spawn5("gemini", args, {
2209
2360
  cwd: ctx.workingDirectory,
2210
2361
  stdio: ["pipe", "pipe", "pipe"],
@@ -2506,6 +2657,279 @@ function detectKimiModels(home = os2.homedir()) {
2506
2657
  return { models, default: defaultModel };
2507
2658
  }
2508
2659
 
2660
+ // src/drivers/opencode.ts
2661
+ import { spawn as spawn7 } from "child_process";
2662
+ import { readFileSync as readFileSync3 } from "fs";
2663
+ import os3 from "os";
2664
+ import path9 from "path";
2665
+ var CHAT_MCP_SERVER_NAME = "chat";
2666
+ var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
2667
+ var SLOCK_AGENT_NAME = "slock";
2668
+ var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2669
+ var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2670
+ var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
2671
+ function buildChatBridgeCommand(ctx) {
2672
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2673
+ return [
2674
+ isTsSource ? "npx" : "node",
2675
+ ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
2676
+ "--agent-id",
2677
+ ctx.agentId,
2678
+ "--server-url",
2679
+ ctx.config.serverUrl,
2680
+ "--auth-token",
2681
+ ctx.config.authToken || ctx.daemonApiKey,
2682
+ "--runtime",
2683
+ "opencode",
2684
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2685
+ "--runtime-actions-only"
2686
+ ];
2687
+ }
2688
+ function parseOpenCodeConfigContent(raw) {
2689
+ if (!raw) return {};
2690
+ try {
2691
+ const parsed = JSON.parse(raw);
2692
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2693
+ return parsed;
2694
+ }
2695
+ } catch {
2696
+ }
2697
+ return {};
2698
+ }
2699
+ function parseUserOpenCodeConfig(ctx) {
2700
+ const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2701
+ return parseOpenCodeConfigContent(raw);
2702
+ }
2703
+ function readLocalOpenCodeConfig(home = os3.homedir()) {
2704
+ const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2705
+ try {
2706
+ return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
2707
+ } catch {
2708
+ }
2709
+ return {};
2710
+ }
2711
+ function recordField(value) {
2712
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2713
+ }
2714
+ function parseSemver(version) {
2715
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
2716
+ if (!match) return null;
2717
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
2718
+ }
2719
+ function isSupportedOpenCodeVersion(version) {
2720
+ if (!version) return true;
2721
+ const actual = parseSemver(version);
2722
+ const minimum = parseSemver(MIN_SUPPORTED_OPENCODE_VERSION);
2723
+ if (!actual || !minimum) return true;
2724
+ for (let i = 0; i < 3; i += 1) {
2725
+ if (actual[i] > minimum[i]) return true;
2726
+ if (actual[i] < minimum[i]) return false;
2727
+ }
2728
+ return true;
2729
+ }
2730
+ function unsupportedOpenCodeVersionMessage(version) {
2731
+ if (!version || isSupportedOpenCodeVersion(version)) return null;
2732
+ return `OpenCode CLI ${version} is unsupported; requires OpenCode >= ${MIN_SUPPORTED_OPENCODE_VERSION}. Upgrade opencode before starting this runtime.`;
2733
+ }
2734
+ function mergeOpenCodeConfigs(localConfig, envConfig) {
2735
+ return {
2736
+ ...localConfig,
2737
+ ...envConfig,
2738
+ provider: {
2739
+ ...recordField(localConfig.provider),
2740
+ ...recordField(envConfig.provider)
2741
+ },
2742
+ agent: {
2743
+ ...recordField(localConfig.agent),
2744
+ ...recordField(envConfig.agent)
2745
+ },
2746
+ mcp: {
2747
+ ...recordField(localConfig.mcp),
2748
+ ...recordField(envConfig.mcp)
2749
+ }
2750
+ };
2751
+ }
2752
+ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2753
+ const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2754
+ const userAgents = recordField(userConfig.agent);
2755
+ const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
2756
+ return {
2757
+ ...userConfig,
2758
+ $schema: "https://opencode.ai/config.json",
2759
+ agent: {
2760
+ ...userAgents,
2761
+ [SLOCK_AGENT_NAME]: {
2762
+ ...userSlockAgent,
2763
+ description: "Slock agent runtime",
2764
+ prompt: ctx.standingPrompt
2765
+ }
2766
+ },
2767
+ mcp: {
2768
+ ...recordField(userConfig.mcp),
2769
+ [CHAT_MCP_SERVER_NAME]: {
2770
+ type: "local",
2771
+ command: buildChatBridgeCommand(ctx),
2772
+ enabled: true
2773
+ }
2774
+ }
2775
+ };
2776
+ }
2777
+ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2778
+ const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2779
+ const config = buildOpenCodeConfig(ctx, home);
2780
+ const env = {
2781
+ ...slock.spawnEnv,
2782
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config)
2783
+ };
2784
+ const args = [
2785
+ "run",
2786
+ "--format",
2787
+ "json",
2788
+ "--dangerously-skip-permissions",
2789
+ "--pure",
2790
+ "--dir",
2791
+ ctx.workingDirectory
2792
+ ];
2793
+ if (ctx.config.model && ctx.config.model !== "default") {
2794
+ args.push("--model", ctx.config.model);
2795
+ }
2796
+ args.push("--agent", SLOCK_AGENT_NAME);
2797
+ if (ctx.config.sessionId) {
2798
+ args.push("--session", ctx.config.sessionId);
2799
+ }
2800
+ const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT : ctx.prompt;
2801
+ args.push("--", turnPrompt);
2802
+ return { args, env, config };
2803
+ }
2804
+ function detectOpenCodeModels(home = os3.homedir()) {
2805
+ const models = [...RUNTIME_MODELS.opencode || []];
2806
+ const providers = recordField(readLocalOpenCodeConfig(home).provider);
2807
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
2808
+ const providerModels = recordField(recordField(providerConfig).models);
2809
+ for (const [modelId, modelConfig] of Object.entries(providerModels)) {
2810
+ const fullId = `${providerId}/${modelId}`;
2811
+ if (models.some((model2) => model2.id === fullId)) continue;
2812
+ const model = recordField(modelConfig);
2813
+ const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
2814
+ models.push({ id: fullId, label: name });
2815
+ }
2816
+ }
2817
+ return models.length > 0 ? { models } : null;
2818
+ }
2819
+ function isSystemFirstMessageTask(message) {
2820
+ return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2821
+ }
2822
+ function buildOpenCodeSystemPrompt(config) {
2823
+ return buildCliTransportSystemPrompt(config, {
2824
+ toolPrefix: CHAT_MCP_TOOL_PREFIX,
2825
+ extraCriticalRules: [
2826
+ "- 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 `chat_runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2827
+ ],
2828
+ postStartupNotes: [
2829
+ "**OpenCode runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
2830
+ ],
2831
+ includeStdinNotificationSection: false,
2832
+ messageNotificationStyle: "poll"
2833
+ });
2834
+ }
2835
+ var OpenCodeDriver = class {
2836
+ id = "opencode";
2837
+ supportsStdinNotification = false;
2838
+ mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
2839
+ busyDeliveryMode = "none";
2840
+ terminateProcessOnTurnEnd = true;
2841
+ deferSpawnUntilMessage = true;
2842
+ usesSlockCliForCommunication = true;
2843
+ shouldDeferWakeMessage(message) {
2844
+ return isSystemFirstMessageTask(message);
2845
+ }
2846
+ sessionId = null;
2847
+ sessionAnnounced = false;
2848
+ probe() {
2849
+ if (!resolveCommandOnPath("opencode")) return { available: false };
2850
+ const version = readCommandVersion("opencode") || void 0;
2851
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
2852
+ if (unsupportedMessage) {
2853
+ return {
2854
+ available: false,
2855
+ version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
2856
+ };
2857
+ }
2858
+ return { available: true, version };
2859
+ }
2860
+ async detectModels() {
2861
+ return detectOpenCodeModels();
2862
+ }
2863
+ spawn(ctx) {
2864
+ this.sessionId = ctx.config.sessionId || null;
2865
+ this.sessionAnnounced = false;
2866
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(readCommandVersion("opencode"));
2867
+ if (unsupportedMessage) {
2868
+ throw new Error(unsupportedMessage);
2869
+ }
2870
+ const launch = buildOpenCodeLaunchOptions(ctx);
2871
+ const proc = spawn7("opencode", launch.args, {
2872
+ cwd: ctx.workingDirectory,
2873
+ stdio: ["pipe", "pipe", "pipe"],
2874
+ env: launch.env,
2875
+ shell: process.platform === "win32"
2876
+ });
2877
+ proc.stdin?.end();
2878
+ return { process: proc };
2879
+ }
2880
+ parseLine(line) {
2881
+ let event;
2882
+ try {
2883
+ event = JSON.parse(line);
2884
+ } catch {
2885
+ return [];
2886
+ }
2887
+ const events = [];
2888
+ if (event.sessionID && event.sessionID !== this.sessionId) {
2889
+ this.sessionId = event.sessionID;
2890
+ }
2891
+ if (!this.sessionAnnounced && this.sessionId) {
2892
+ events.push({ kind: "session_init", sessionId: this.sessionId });
2893
+ this.sessionAnnounced = true;
2894
+ }
2895
+ switch (event.type) {
2896
+ case "step_start":
2897
+ events.push({ kind: "thinking", text: "" });
2898
+ break;
2899
+ case "text":
2900
+ if (typeof event.part?.text === "string" && event.part.text.length > 0) {
2901
+ events.push({ kind: "text", text: event.part.text });
2902
+ }
2903
+ break;
2904
+ case "tool_use":
2905
+ events.push({
2906
+ kind: "tool_call",
2907
+ name: event.part?.tool || "unknown_tool",
2908
+ input: event.part?.state?.input
2909
+ });
2910
+ break;
2911
+ case "step_finish":
2912
+ if (event.part?.reason !== "tool-calls") {
2913
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
2914
+ }
2915
+ break;
2916
+ case "error": {
2917
+ const message = event.error?.data?.message || event.error?.message || (event.error?.name ? `${event.error.name} (no message)` : null) || "Unknown OpenCode error";
2918
+ events.push({ kind: "error", message });
2919
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
2920
+ break;
2921
+ }
2922
+ }
2923
+ return events;
2924
+ }
2925
+ encodeStdinMessage(_text, _sessionId, _opts) {
2926
+ return null;
2927
+ }
2928
+ buildSystemPrompt(config, _agentId) {
2929
+ return buildOpenCodeSystemPrompt(config);
2930
+ }
2931
+ };
2932
+
2509
2933
  // src/drivers/index.ts
2510
2934
  var driverFactories = {
2511
2935
  claude: () => new ClaudeDriver(),
@@ -2513,7 +2937,8 @@ var driverFactories = {
2513
2937
  copilot: () => new CopilotDriver(),
2514
2938
  cursor: () => new CursorDriver(),
2515
2939
  gemini: () => new GeminiDriver(),
2516
- kimi: () => new KimiDriver()
2940
+ kimi: () => new KimiDriver(),
2941
+ opencode: () => new OpenCodeDriver()
2517
2942
  };
2518
2943
  function getDriver(runtimeId) {
2519
2944
  const createDriver = driverFactories[runtimeId];
@@ -2526,7 +2951,7 @@ function getDriver(runtimeId) {
2526
2951
 
2527
2952
  // src/workspaces.ts
2528
2953
  import { readdir, rm, stat } from "fs/promises";
2529
- import path9 from "path";
2954
+ import path10 from "path";
2530
2955
  function isValidWorkspaceDirectoryName(directoryName) {
2531
2956
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
2532
2957
  }
@@ -2534,7 +2959,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
2534
2959
  if (!isValidWorkspaceDirectoryName(directoryName)) {
2535
2960
  return null;
2536
2961
  }
2537
- return path9.join(dataDir, directoryName);
2962
+ return path10.join(dataDir, directoryName);
2538
2963
  }
2539
2964
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
2540
2965
  return {
@@ -2583,7 +3008,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
2583
3008
  return summary;
2584
3009
  }
2585
3010
  const childSummaries = await Promise.all(
2586
- entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
3011
+ entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
2587
3012
  );
2588
3013
  for (const childSummary of childSummaries) {
2589
3014
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -2602,7 +3027,7 @@ async function scanWorkspaceDirectories(dataDir) {
2602
3027
  if (!entry.isDirectory()) {
2603
3028
  return null;
2604
3029
  }
2605
- const dirPath = path9.join(dataDir, entry.name);
3030
+ const dirPath = path10.join(dataDir, entry.name);
2606
3031
  try {
2607
3032
  const summary = await summarizeWorkspaceDirectory(dirPath);
2608
3033
  return {
@@ -2634,7 +3059,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
2634
3059
  }
2635
3060
 
2636
3061
  // src/agentProcessManager.ts
2637
- var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
3062
+ var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
2638
3063
  function toLocalTime(iso) {
2639
3064
  const d = new Date(iso);
2640
3065
  if (isNaN(d.getTime())) return iso;
@@ -2660,6 +3085,86 @@ function formatMessageTarget(message) {
2660
3085
  function getMessageShortId(messageId) {
2661
3086
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
2662
3087
  }
3088
+ function findSessionJsonl(root, predicate) {
3089
+ let visited = 0;
3090
+ const maxEntries = 1e4;
3091
+ const maxDepth = 8;
3092
+ const visit = (dir, depth) => {
3093
+ if (depth < 0 || visited >= maxEntries) return null;
3094
+ let entries;
3095
+ try {
3096
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3097
+ } catch {
3098
+ return null;
3099
+ }
3100
+ for (const entry of entries) {
3101
+ if (++visited > maxEntries) return null;
3102
+ if (!entry.isFile() || !predicate(entry.name)) continue;
3103
+ return path11.join(dir, entry.name);
3104
+ }
3105
+ for (const entry of entries) {
3106
+ if (++visited > maxEntries) return null;
3107
+ if (!entry.isDirectory()) continue;
3108
+ const found = visit(path11.join(dir, entry.name), depth - 1);
3109
+ if (found) return found;
3110
+ }
3111
+ return null;
3112
+ };
3113
+ return visit(root, maxDepth);
3114
+ }
3115
+ function safeSessionFilename(value) {
3116
+ const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3117
+ return normalized || "unknown-session";
3118
+ }
3119
+ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3120
+ try {
3121
+ const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
3122
+ mkdirSync4(dir, { recursive: true });
3123
+ const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3124
+ writeFileSync7(filePath, JSON.stringify({
3125
+ type: "runtime_session_handoff",
3126
+ runtime,
3127
+ sessionId,
3128
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3129
+ 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."
3130
+ }) + "\n", { mode: 384 });
3131
+ return {
3132
+ label: sessionId,
3133
+ path: filePath,
3134
+ runtime,
3135
+ reachable: true,
3136
+ reason: "native session file path not found; using daemon handoff file"
3137
+ };
3138
+ } catch {
3139
+ return null;
3140
+ }
3141
+ }
3142
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3143
+ const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3144
+ if (directPath) {
3145
+ try {
3146
+ if (statSync(directPath).isFile()) {
3147
+ return { label: sessionId, path: directPath, runtime, reachable: true };
3148
+ }
3149
+ } catch {
3150
+ }
3151
+ }
3152
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path11.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path11.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
3153
+ if (!resolvedPath && fallbackDir) {
3154
+ const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
3155
+ if (fallback) return fallback;
3156
+ }
3157
+ const ref = {
3158
+ label: sessionId,
3159
+ path: resolvedPath ?? sessionId,
3160
+ runtime,
3161
+ reachable: Boolean(resolvedPath)
3162
+ };
3163
+ if (!resolvedPath) {
3164
+ ref.reason = "session file path not found";
3165
+ }
3166
+ return ref;
3167
+ }
2663
3168
  function formatSenderHandle(message) {
2664
3169
  return message.sender_description ? `@${message.sender_name} \u2014 ${message.sender_description}` : `@${message.sender_name}`;
2665
3170
  }
@@ -2675,12 +3180,38 @@ function formatThreadContextMessage(message) {
2675
3180
  const senderType = formatVisibleActorType(message.sender_type);
2676
3181
  return `- [msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}`;
2677
3182
  }
2678
- function formatIncomingMessage(message) {
3183
+ function mcpToolName(driver, name) {
3184
+ return `${driver?.mcpToolPrefix ?? ""}${name}`;
3185
+ }
3186
+ function communicationCommand(driver, name) {
3187
+ if (!driver?.usesSlockCliForCommunication) {
3188
+ return mcpToolName(driver, name);
3189
+ }
3190
+ switch (name) {
3191
+ case "send_message":
3192
+ return "slock message send";
3193
+ case "read_history":
3194
+ return "slock message read";
3195
+ case "check_messages":
3196
+ return "slock message check";
3197
+ case "claim_tasks":
3198
+ return "slock task claim";
3199
+ default:
3200
+ return `slock ${name.replace(/_/g, " ")}`;
3201
+ }
3202
+ }
3203
+ function dynamicReplyInstruction(driver) {
3204
+ return driver?.usesSlockCliForCommunication ? "reply using `slock message send --target <exact target>`" : `reply with ${communicationCommand(driver, "send_message")}`;
3205
+ }
3206
+ function dynamicClaimInstruction(driver) {
3207
+ return driver?.usesSlockCliForCommunication ? "claim the relevant task with `slock task claim`" : `claim the relevant task with ${communicationCommand(driver, "claim_tasks")}`;
3208
+ }
3209
+ function formatIncomingMessage(message, driver) {
2679
3210
  const threadJoinPrefix = message.thread_join_context ? [
2680
3211
  `[System: You were added to a new thread via @mention. Read this context before replying.]`,
2681
3212
  `parent: ${message.thread_join_context.parent_target}`,
2682
3213
  `thread: ${message.thread_join_context.thread_target}`,
2683
- `suggested next step: read_history(channel="${message.thread_join_context.suggested_read_history_target}")`,
3214
+ `suggested next step: ${driver?.usesSlockCliForCommunication ? `slock message read --channel "${message.thread_join_context.suggested_read_history_target}"` : `${communicationCommand(driver, "read_history")}(channel="${message.thread_join_context.suggested_read_history_target}")`}`,
2684
3215
  "",
2685
3216
  "Parent message:",
2686
3217
  formatThreadContextMessage(message.thread_join_context.parent_message),
@@ -2699,6 +3230,42 @@ function formatIncomingMessage(message) {
2699
3230
  return threadJoinPrefix ? `${threadJoinPrefix}
2700
3231
  ${body}` : body;
2701
3232
  }
3233
+ function formatRuntimeProfileControlPrompt(messages) {
3234
+ const controls = messages.map((message) => ({
3235
+ message,
3236
+ notification: runtimeProfileNotificationFromMessage(message)
3237
+ }));
3238
+ if (controls.length === 0 || controls.some(({ notification }) => !notification)) {
3239
+ return null;
3240
+ }
3241
+ const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
3242
+ return [
3243
+ "Runtime Profile control notice.",
3244
+ "",
3245
+ "Complete the required runtime control action before reading or responding to normal inbox messages.",
3246
+ "",
3247
+ body,
3248
+ "",
3249
+ "Do not answer this notice in prose as the acknowledgment."
3250
+ ].join("\n");
3251
+ }
3252
+ function formatRuntimeProfileControlStartupInput(control, driver) {
3253
+ if (control.kind !== "migration") {
3254
+ return [
3255
+ "Read the Runtime Profile daemon release notice from your system prompt before normal work.",
3256
+ "No chat reply is required for this notice. Stop after reading it; queued inbox messages will be delivered separately."
3257
+ ].join("\n");
3258
+ }
3259
+ const actionName = `${driver.mcpToolPrefix}runtime_profile_migration_done`;
3260
+ return [
3261
+ "Runtime Profile migration is required before normal work.",
3262
+ `Invoke the available runtime control action \`${actionName}\` with exactly this migration_key: \`${control.key}\`.`,
3263
+ "Do not read MEMORY.md, check messages, or send a chat reply before this tool call.",
3264
+ "After the runtime control action succeeds, stop. Queued inbox messages will be delivered separately.",
3265
+ "",
3266
+ control.message
3267
+ ].join("\n");
3268
+ }
2702
3269
  function buildUnreadSummary(messages, excludeChannel) {
2703
3270
  const summary = /* @__PURE__ */ new Map();
2704
3271
  for (const message of messages) {
@@ -2723,6 +3290,16 @@ var FIRST_CINDY_SEED_MODE = "first-cindy";
2723
3290
  function getOnboardingSeedMode(config) {
2724
3291
  return (config.envVars?.[ONBOARDING_MEMORY_SEED_ENV] || "").trim().toLowerCase();
2725
3292
  }
3293
+ function withLocalRuntimeContext(config, agentId, workspacePath) {
3294
+ return {
3295
+ ...config,
3296
+ runtimeContext: {
3297
+ ...config.runtimeContext ?? {},
3298
+ agentId: config.runtimeContext?.agentId ?? agentId,
3299
+ workspacePath
3300
+ }
3301
+ };
3302
+ }
2726
3303
  function buildOnboardingPlaybookMd() {
2727
3304
  return `# Cindy Onboarding Playbook
2728
3305
 
@@ -2731,14 +3308,18 @@ Start warm and brief.
2731
3308
  Move quickly to one useful action, not a feature tour.
2732
3309
  Keep activation energy low: invite the user to start with one sentence about what they need now.
2733
3310
 
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?
3311
+ ## Step 2: Activate or Propose
3312
+ Use one decision: does the user already know what they want to do?
3313
+ - Yes: skip role/work intake and propose a starter plan.
3314
+ - No: ask what they do and what they are working on. These questions are activation, not a questionnaire.
3315
+
3316
+ After any usable signal, stop asking and propose.
3317
+ After confirming language preference, do not give a generic product introduction; move into the user's work or a starter action.
2738
3318
 
2739
3319
  ## Step 3: Route by Intent (A-E)
2740
3320
  - A: Specific project/task
2741
- - Map immediately to first setup actions (agents + channels + first task).
3321
+ - Enter starter-task mode immediately.
3322
+ - Propose first setup actions before asking for more detail.
2742
3323
  - B: "What can you do?" curiosity
2743
3324
  - Proactively share 1-2 interview-grounded examples, then ask the user to pick one.
2744
3325
  - Use this opener tone: "Here are some examples our users have shared with us. I'm sharing these to inspire you."
@@ -2749,6 +3330,28 @@ Ask only what is needed to route:
2749
3330
  - E: Low-intent greeting/testing
2750
3331
  - Use a low-pressure prompt and guide to one concrete starter action.
2751
3332
 
3333
+ ### Starter Plan Output
3334
+ A starter plan should make the next action executable, not just descriptive:
3335
+ - agent name + role description that can be copied into the create-agent form
3336
+ - suggested channel or workstream
3337
+ - first task to send after creation
3338
+ - next UI action if the user needs to create an agent or channel
3339
+
3340
+ Do not use a rigid keyword routing table. Use examples as inspiration, then adapt to the user's context.
3341
+ If details are missing but not blocking, state reasonable defaults and invite correction.
3342
+ Only ask one blocking question first if the answer is required before any useful starter plan can be proposed.
3343
+ Do not imply you have already created agents or channels unless the action has actually happened.
3344
+
3345
+ ### Capability Boundary Pivot
3346
+ If the user's primary request is outside current capabilities, acknowledge the limitation once and pivot immediately to the nearest useful alternative.
3347
+ Do not repeat that something is impossible across multiple turns.
3348
+ Offer a concrete substitute: a manual input path, a narrower analysis task, an agent/team setup, or another workflow Slock can execute now.
3349
+
3350
+ ### Active-Elsewhere Handoff
3351
+ Channel silence is not failure.
3352
+ If the user is already active outside the onboarding channel, follow the work instead of trying to pull them back.
3353
+ Offer a concrete next step in the context they are using: first task, second agent suggestion, channel structure, or reminder.
3354
+
2752
3355
  ## Step 4: Progress Setup (Soft Guidance)
2753
3356
  While helping with real work, progressively shape:
2754
3357
  - initial team target >= 3 agents
@@ -2762,6 +3365,8 @@ Do not force setup before value.
2762
3365
 
2763
3366
  ## Step 5: End Every Turn with One Next Step
2764
3367
  Each reply should end with one clear, immediate action.
3368
+ At wrap-up, if there is a concrete next check-in, ask consent to set one contextual reminder.
3369
+ The reminder must reference the user's goal, agent, recent step, or suggested next action; do not send generic "come back later" reminders.
2765
3370
 
2766
3371
  ## Inspiration Stories (Interview-Grounded)
2767
3372
  - Story 1: "Sense of abundance" \u2014 agents self-organize, you do not need to micro-manage.
@@ -2954,6 +3559,34 @@ Do not copy these answers verbatim.
2954
3559
 
2955
3560
  ### Guardrail
2956
3561
  - Keep contact guidance concrete and current; do not invent alternative support channels.
3562
+
3563
+ ## FAQ 14: Can I use Slock on my phone?
3564
+ ### Answer idea
3565
+ - Yes. Slock can be used from a mobile browser.
3566
+ - For easier return access, users can add Slock to their phone home screen as a web app:
3567
+ - iPhone: Safari \u2192 Share \u2192 Add to Home Screen
3568
+ - Android: Chrome \u2192 menu \u2192 Add to Home Screen / Install app
3569
+ - Good mobile use cases: quick check-ins, todos/reminders, short replies, and reviewing agent updates.
3570
+
3571
+ ### Next step
3572
+ - If the user wants mobile access now, ask whether they use iPhone or Android, then guide the matching Add to Home Screen step.
3573
+
3574
+ ### Guardrail
3575
+ - Do not imply Slock has a native iOS/Android App Store app.
3576
+ - Do not over-sell it as fully equivalent to a native app; call it mobile browser / home-screen web app.
3577
+
3578
+ ## FAQ 15: How do I create agents or channels?
3579
+ ### Answer idea
3580
+ - 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.
3581
+ - The user can create channels from the Channels section by clicking the + button.
3582
+ - When recommending setup, provide copyable agent names/descriptions, suggested channel names, and the first task to send after creation.
3583
+
3584
+ ### Next step
3585
+ - Give the exact agent/channel spec the user can copy, then guide them to the relevant + button or creation dialog.
3586
+
3587
+ ### Guardrail
3588
+ - Do not imply you already created agents or channels unless that action actually happened.
3589
+ - If you cannot directly create something, avoid a long permissions explanation; give the copyable spec and the next UI action.
2957
3590
  `;
2958
3591
  }
2959
3592
  function buildOnboardingSeedFiles() {
@@ -3112,9 +3745,20 @@ function classifyTerminalFailure(ap) {
3112
3745
  return null;
3113
3746
  }
3114
3747
  function isMissingResumeSession(ap) {
3115
- if (ap.driver.id !== "claude") return false;
3116
3748
  if (!ap.sessionId) return false;
3117
- return /No conversation found with session ID/i.test(ap.lastRuntimeError || "");
3749
+ const candidates = [
3750
+ ap.lastRuntimeError,
3751
+ ...ap.recentStderr
3752
+ ].filter((value) => !!value);
3753
+ if (ap.driver.id === "claude") {
3754
+ return candidates.some((text) => /No conversation found with session ID/i.test(text));
3755
+ }
3756
+ if (ap.driver.id === "opencode") {
3757
+ return candidates.some(
3758
+ (text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
3759
+ );
3760
+ }
3761
+ return false;
3118
3762
  }
3119
3763
  function getMessageDeliveryText(driver) {
3120
3764
  return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
@@ -3129,7 +3773,12 @@ function getBusyDeliveryNote(driver) {
3129
3773
  }
3130
3774
  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
3775
  }
3132
- var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
3776
+ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = (
3777
+ // Claude Code 2.1.114 treats "follow your system prompt" style user turns as
3778
+ // prompt-injection bait even when the prompt is supplied via --system-prompt-file.
3779
+ // A neutral starter lets the native standing prompt drive startup/migration.
3780
+ "Start."
3781
+ );
3133
3782
  var AgentProcessManager = class _AgentProcessManager {
3134
3783
  agents = /* @__PURE__ */ new Map();
3135
3784
  agentsStarting = /* @__PURE__ */ new Set();
@@ -3143,6 +3792,7 @@ var AgentProcessManager = class _AgentProcessManager {
3143
3792
  daemonApiKey;
3144
3793
  serverUrl;
3145
3794
  dataDir;
3795
+ runtimeSessionHomeDir;
3146
3796
  driverResolver;
3147
3797
  defaultAgentEnvVarsProvider;
3148
3798
  tracer;
@@ -3153,6 +3803,7 @@ var AgentProcessManager = class _AgentProcessManager {
3153
3803
  this.daemonApiKey = daemonApiKey;
3154
3804
  this.serverUrl = opts.serverUrl;
3155
3805
  this.dataDir = opts.dataDir || DATA_DIR;
3806
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
3156
3807
  this.driverResolver = opts.driverResolver || getDriver;
3157
3808
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3158
3809
  this.tracer = opts.tracer ?? noopTracer;
@@ -3169,41 +3820,45 @@ var AgentProcessManager = class _AgentProcessManager {
3169
3820
  this.agentsStarting.add(agentId);
3170
3821
  try {
3171
3822
  const driver = this.driverResolver(config.runtime || "claude");
3172
- const agentDataDir = path10.join(this.dataDir, agentId);
3823
+ const agentDataDir = path11.join(this.dataDir, agentId);
3173
3824
  await mkdir(agentDataDir, { recursive: true });
3174
- const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
3825
+ const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
3826
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
3175
3827
  try {
3176
3828
  await access(memoryMdPath);
3177
3829
  } catch {
3178
- const initialMemoryMd = buildInitialMemoryMd(config);
3830
+ const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
3179
3831
  await writeFile(memoryMdPath, initialMemoryMd);
3180
3832
  }
3181
- const notesDir = path10.join(agentDataDir, "notes");
3833
+ const notesDir = path11.join(agentDataDir, "notes");
3182
3834
  await mkdir(notesDir, { recursive: true });
3183
3835
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
3184
3836
  const seedFiles = buildOnboardingSeedFiles();
3185
3837
  for (const { relativePath, content } of seedFiles) {
3186
- const fullPath = path10.join(agentDataDir, relativePath);
3838
+ const fullPath = path11.join(agentDataDir, relativePath);
3187
3839
  try {
3188
3840
  await access(fullPath);
3189
3841
  } catch {
3190
- await mkdir(path10.dirname(fullPath), { recursive: true });
3842
+ await mkdir(path11.dirname(fullPath), { recursive: true });
3191
3843
  await writeFile(fullPath, content);
3192
3844
  }
3193
3845
  }
3194
3846
  }
3195
- const isResume = !!config.sessionId;
3196
- const standingPrompt = driver.buildSystemPrompt(config, agentId);
3847
+ const isResume = !!runtimeConfig.sessionId;
3848
+ const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
3197
3849
  let prompt;
3198
- if (isResume && resumePrompt) {
3850
+ if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
3851
+ prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
3852
+ } else if (isResume && resumePrompt) {
3199
3853
  prompt = resumePrompt;
3200
3854
  prompt += getBusyDeliveryNote(driver);
3201
3855
  } else if (wakeMessage) {
3856
+ const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
3202
3857
  const channelLabel = formatChannelLabel(wakeMessage);
3203
- prompt = `New message received:
3858
+ prompt = runtimeProfileControlPrompt ?? `New message received:
3204
3859
 
3205
- ${formatIncomingMessage(wakeMessage)}`;
3206
- if (unreadSummary && Object.keys(unreadSummary).length > 0) {
3860
+ ${formatIncomingMessage(wakeMessage, driver)}`;
3861
+ if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
3207
3862
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
3208
3863
  if (otherUnread.length > 0) {
3209
3864
  prompt += `
@@ -3215,15 +3870,17 @@ You also have unread messages in other channels:`;
3215
3870
  }
3216
3871
  prompt += `
3217
3872
 
3218
- Use read_history to catch up, or respond to the message above first.`;
3873
+ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to the message above first.`;
3219
3874
  }
3220
3875
  }
3221
- prompt += `
3876
+ if (!runtimeProfileControlPrompt) {
3877
+ prompt += `
3222
3878
 
3223
- Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
3879
+ Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
3224
3880
 
3225
3881
  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);
3882
+ prompt += getBusyDeliveryNote(driver);
3883
+ }
3227
3884
  } else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
3228
3885
  prompt = `You have unread messages from while you were offline:`;
3229
3886
  for (const [ch, count] of Object.entries(unreadSummary)) {
@@ -3232,14 +3889,32 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
3232
3889
  }
3233
3890
  prompt += `
3234
3891
 
3235
- Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: reply with send_message and claim the relevant task before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver)}`;
3892
+ Use ${communicationCommand(driver, "read_history")} to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call ${communicationCommand(driver, "check_messages")} in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: ${dynamicReplyInstruction(driver)} and ${dynamicClaimInstruction(driver)} before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver)}`;
3236
3893
  } else if (isResume) {
3237
3894
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
3238
3895
  prompt += getBusyDeliveryNote(driver);
3239
3896
  } else {
3240
3897
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
3241
3898
  }
3242
- const effectiveConfig = await this.buildSpawnConfig(agentId, config);
3899
+ const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
3900
+ const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
3901
+ if (canDeferEmptyStart) {
3902
+ const pendingMessages = this.startingInboxes.get(agentId) || [];
3903
+ this.startingInboxes.delete(agentId);
3904
+ this.agentsStarting.delete(agentId);
3905
+ this.idleAgentConfigs.set(agentId, {
3906
+ config: effectiveConfig,
3907
+ sessionId: effectiveConfig.sessionId || null,
3908
+ launchId: launchId || null
3909
+ });
3910
+ this.sendAgentStatus(agentId, "active", launchId || null);
3911
+ this.broadcastActivity(agentId, "online", "Process idle");
3912
+ logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
3913
+ for (const message of pendingMessages) {
3914
+ this.deliverMessage(agentId, message);
3915
+ }
3916
+ return;
3917
+ }
3243
3918
  const { process: proc } = driver.spawn({
3244
3919
  agentId,
3245
3920
  config: effectiveConfig,
@@ -3255,9 +3930,12 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3255
3930
  process: proc,
3256
3931
  driver,
3257
3932
  inbox: this.startingInboxes.get(agentId) || [],
3258
- config,
3259
- sessionId: config.sessionId || null,
3933
+ config: runtimeConfig,
3934
+ sessionId: runtimeConfig.sessionId || null,
3260
3935
  launchId: launchId || null,
3936
+ startupWakeMessage: wakeMessage,
3937
+ startupUnreadSummary: unreadSummary,
3938
+ startupResumePrompt: resumePrompt,
3261
3939
  isIdle: false,
3262
3940
  notificationTimer: null,
3263
3941
  pendingNotificationCount: 0,
@@ -3275,6 +3953,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3275
3953
  spawnError: null,
3276
3954
  exitCode: null,
3277
3955
  exitSignal: null,
3956
+ expectedTerminationReason: null,
3278
3957
  pendingTrajectory: null,
3279
3958
  gatedSteering: createGatedSteeringState()
3280
3959
  };
@@ -3282,6 +3961,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3282
3961
  this.agents.set(agentId, agentProcess);
3283
3962
  this.startRuntimeTrace(agentId, agentProcess, "spawn");
3284
3963
  this.agentsStarting.delete(agentId);
3964
+ if (config.runtimeProfileControl) {
3965
+ this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
3966
+ }
3285
3967
  if (wakeMessage) {
3286
3968
  this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
3287
3969
  }
@@ -3341,20 +4023,60 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3341
4023
  }
3342
4024
  const finalCode = ap.exitCode ?? code;
3343
4025
  const finalSignal = ap.exitSignal ?? signal;
3344
- const terminalFailureDetail = classifyTerminalFailure(ap);
3345
- this.endRuntimeTrace(ap, finalCode === 0 ? "ok" : "error", {
3346
- outcome: finalCode === 0 ? "process-exit" : "process-crash",
4026
+ const expectedTermination = Boolean(ap.expectedTerminationReason);
4027
+ const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
4028
+ const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
4029
+ const missingResumeSession = isMissingResumeSession(ap);
4030
+ const summary = summarizeCrash(finalCode, finalSignal);
4031
+ this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
4032
+ outcome: processEndedCleanly ? "process-exit" : "process-crash",
4033
+ expectedTerminationReason: ap.expectedTerminationReason || void 0,
3347
4034
  exitCode: finalCode,
3348
4035
  exitSignal: finalSignal
3349
4036
  });
3350
- if (finalCode === 0) {
4037
+ if (processEndedCleanly) {
3351
4038
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
3352
4039
  } else {
3353
4040
  this.clearCompactionWatchdog(ap);
3354
4041
  }
3355
4042
  this.agents.delete(agentId);
3356
- if (finalCode === 0) {
3357
- const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
4043
+ if (missingResumeSession) {
4044
+ const staleSessionId = ap.sessionId;
4045
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : "Claude";
4046
+ const restartConfig = { ...ap.config, sessionId: null };
4047
+ logger.warn(
4048
+ `[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
4049
+ );
4050
+ this.broadcastActivity(
4051
+ agentId,
4052
+ "working",
4053
+ `Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
4054
+ [{ kind: "text", text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
4055
+ );
4056
+ this.startAgent(
4057
+ agentId,
4058
+ restartConfig,
4059
+ ap.startupWakeMessage,
4060
+ ap.startupUnreadSummary,
4061
+ ap.startupResumePrompt,
4062
+ ap.launchId || void 0
4063
+ ).catch((err) => {
4064
+ logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
4065
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
4066
+ this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
4067
+ });
4068
+ return;
4069
+ }
4070
+ if (processEndedCleanly) {
4071
+ let queuedWakeMessage;
4072
+ if (!ap.driver.supportsStdinNotification) {
4073
+ while (ap.inbox.length > 0) {
4074
+ const candidate = ap.inbox.shift();
4075
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
4076
+ queuedWakeMessage = candidate;
4077
+ break;
4078
+ }
4079
+ }
3358
4080
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
3359
4081
  if (queuedWakeMessage) {
3360
4082
  logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
@@ -3389,26 +4111,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3389
4111
  } else {
3390
4112
  this.idleAgentConfigs.delete(agentId);
3391
4113
  const reason = formatCrashReason(finalCode, finalSignal, ap);
3392
- const summary = summarizeCrash(finalCode, finalSignal);
3393
- if (isMissingResumeSession(ap)) {
3394
- const staleSessionId = ap.sessionId;
3395
- const restartConfig = { ...ap.config, sessionId: null };
3396
- logger.warn(
3397
- `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
3398
- );
3399
- this.broadcastActivity(
3400
- agentId,
3401
- "working",
3402
- "Stored Claude session missing; cold-starting a new session\u2026",
3403
- [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
3404
- );
3405
- this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
3406
- logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
3407
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
3408
- this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
3409
- });
3410
- return;
3411
- }
3412
4114
  logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
3413
4115
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
3414
4116
  if (terminalFailureDetail) {
@@ -3492,6 +4194,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3492
4194
  this.agents.delete(agentId);
3493
4195
  ap.process.kill("SIGTERM");
3494
4196
  if (!silent) {
4197
+ this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
3495
4198
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
3496
4199
  this.broadcastActivity(agentId, "offline", "Stopped");
3497
4200
  logger.info(`[Agent ${agentId}] Stopped by request`);
@@ -3530,6 +4233,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3530
4233
  }
3531
4234
  const cached = this.idleAgentConfigs.get(agentId);
3532
4235
  if (cached) {
4236
+ const driver = this.driverResolver(cached.config.runtime || "claude");
4237
+ if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4238
+ return;
4239
+ }
3533
4240
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
3534
4241
  this.idleAgentConfigs.delete(agentId);
3535
4242
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
@@ -3538,6 +4245,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3538
4245
  }
3539
4246
  return;
3540
4247
  }
4248
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4249
+ return;
4250
+ }
3541
4251
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
3542
4252
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
3543
4253
  nextMessages.push(message);
@@ -3566,7 +4276,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3566
4276
  }
3567
4277
  }
3568
4278
  async resetWorkspace(agentId) {
3569
- const agentDataDir = path10.join(this.dataDir, agentId);
4279
+ const agentDataDir = path11.join(this.dataDir, agentId);
3570
4280
  try {
3571
4281
  await rm2(agentDataDir, { recursive: true, force: true });
3572
4282
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -3582,6 +4292,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3582
4292
  getRunningAgentIds() {
3583
4293
  return [...this.agents.keys()];
3584
4294
  }
4295
+ shouldDeferWakeMessage(agentId, driver, message) {
4296
+ if (!driver.shouldDeferWakeMessage?.(message)) return false;
4297
+ logger.info(`[Agent ${agentId}] Deferred non-concrete wake message for ${driver.id}`);
4298
+ return true;
4299
+ }
3585
4300
  getAgentSessionId(agentId) {
3586
4301
  return this.agents.get(agentId)?.sessionId ?? null;
3587
4302
  }
@@ -3596,7 +4311,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3596
4311
  return result;
3597
4312
  }
3598
4313
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
3599
- const workspacePath = path10.join(this.dataDir, agentId);
4314
+ const workspacePath = path11.join(this.dataDir, agentId);
3600
4315
  return {
3601
4316
  agentId,
3602
4317
  launchId,
@@ -3610,12 +4325,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3610
4325
  path: workspacePath,
3611
4326
  reachable: true
3612
4327
  },
3613
- sessionRef: sessionId ? {
3614
- label: sessionId,
3615
- path: sessionId,
3616
- runtime: config.runtime,
3617
- reachable: true
3618
- } : null
4328
+ sessionRef: sessionId ? resolveRuntimeSessionRef(config.runtime, sessionId, this.runtimeSessionHomeDir, workspacePath) : null
3619
4329
  }
3620
4330
  };
3621
4331
  }
@@ -3706,9 +4416,26 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3706
4416
  }
3707
4417
  }
3708
4418
  }
3709
- sendRuntimeProfileReport(agentId) {
3710
- const report = this.getAgentRuntimeProfileReport(agentId);
3711
- if (!report) return;
4419
+ ackInjectedRuntimeProfileControl(agentId, control, launchId) {
4420
+ const title = runtimeProfileNotificationTitle(control.kind);
4421
+ this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
4422
+ if (control.kind === "migration") {
4423
+ this.sendToServer({
4424
+ type: "agent:runtime_profile:migration:ack",
4425
+ agentId,
4426
+ migrationKey: control.key,
4427
+ launchId: launchId || void 0
4428
+ });
4429
+ } else {
4430
+ this.sendToServer({
4431
+ type: "agent:runtime_profile:daemon_release_notice:ack",
4432
+ agentId,
4433
+ noticeKey: control.key,
4434
+ launchId: launchId || void 0
4435
+ });
4436
+ }
4437
+ }
4438
+ sendRuntimeProfileWireReport(report) {
3712
4439
  this.sendToServer({
3713
4440
  type: "agent:runtime_profile",
3714
4441
  agentId: report.agentId,
@@ -3716,6 +4443,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3716
4443
  launchId: report.launchId || void 0
3717
4444
  });
3718
4445
  }
4446
+ sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4447
+ this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
4448
+ }
4449
+ sendRuntimeProfileReport(agentId) {
4450
+ const report = this.getAgentRuntimeProfileReport(agentId);
4451
+ if (!report) return;
4452
+ this.sendRuntimeProfileWireReport(report);
4453
+ }
3719
4454
  // Machine-level workspace scanning
3720
4455
  async scanAllWorkspaces() {
3721
4456
  return scanWorkspaceDirectories(this.dataDir);
@@ -3725,7 +4460,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3725
4460
  }
3726
4461
  // Workspace file browsing
3727
4462
  async getFileTree(agentId, dirPath) {
3728
- const agentDir = path10.join(this.dataDir, agentId);
4463
+ const agentDir = path11.join(this.dataDir, agentId);
3729
4464
  try {
3730
4465
  await stat2(agentDir);
3731
4466
  } catch {
@@ -3733,8 +4468,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3733
4468
  }
3734
4469
  let targetDir = agentDir;
3735
4470
  if (dirPath) {
3736
- const resolved = path10.resolve(agentDir, dirPath);
3737
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4471
+ const resolved = path11.resolve(agentDir, dirPath);
4472
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
3738
4473
  return [];
3739
4474
  }
3740
4475
  targetDir = resolved;
@@ -3742,9 +4477,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3742
4477
  return this.listDirectoryChildren(targetDir, agentDir);
3743
4478
  }
3744
4479
  async readFile(agentId, filePath) {
3745
- const agentDir = path10.join(this.dataDir, agentId);
3746
- const resolved = path10.resolve(agentDir, filePath);
3747
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4480
+ const agentDir = path11.join(this.dataDir, agentId);
4481
+ const resolved = path11.resolve(agentDir, filePath);
4482
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
3748
4483
  throw new Error("Access denied");
3749
4484
  }
3750
4485
  const info = await stat2(resolved);
@@ -3768,7 +4503,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3768
4503
  ".sh",
3769
4504
  ".py"
3770
4505
  ]);
3771
- const ext = path10.extname(resolved).toLowerCase();
4506
+ const ext = path11.extname(resolved).toLowerCase();
3772
4507
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
3773
4508
  return { content: null, binary: true };
3774
4509
  }
@@ -3794,14 +4529,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3794
4529
  async listSkills(agentId, runtimeHint) {
3795
4530
  const agent = this.agents.get(agentId);
3796
4531
  const runtime = runtimeHint || agent?.config.runtime || "claude";
3797
- const home = os3.homedir();
3798
- const workspaceDir = path10.join(this.dataDir, agentId);
4532
+ const home = os4.homedir();
4533
+ const workspaceDir = path11.join(this.dataDir, agentId);
3799
4534
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
3800
4535
  const globalResults = await Promise.all(
3801
- paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
4536
+ paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
3802
4537
  );
3803
4538
  const workspaceResults = await Promise.all(
3804
- paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
4539
+ paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
3805
4540
  );
3806
4541
  const dedup = (skills) => {
3807
4542
  const seen = /* @__PURE__ */ new Set();
@@ -3830,7 +4565,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3830
4565
  const skills = [];
3831
4566
  for (const entry of entries) {
3832
4567
  if (entry.isDirectory() || entry.isSymbolicLink()) {
3833
- const skillMd = path10.join(dir, entry.name, "SKILL.md");
4568
+ const skillMd = path11.join(dir, entry.name, "SKILL.md");
3834
4569
  try {
3835
4570
  const content = await readFile(skillMd, "utf-8");
3836
4571
  const skill = this.parseSkillMd(entry.name, content);
@@ -3841,7 +4576,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3841
4576
  } else if (entry.name.endsWith(".md")) {
3842
4577
  const cmdName = entry.name.replace(/\.md$/, "");
3843
4578
  try {
3844
- const content = await readFile(path10.join(dir, entry.name), "utf-8");
4579
+ const content = await readFile(path11.join(dir, entry.name), "utf-8");
3845
4580
  const skill = this.parseSkillMd(cmdName, content);
3846
4581
  skill.sourcePath = dir;
3847
4582
  skills.push(skill);
@@ -4056,7 +4791,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4056
4791
  logger.info(
4057
4792
  `[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
4058
4793
  );
4059
- this.broadcastActivity(agentId, "working", "Message received");
4794
+ if (reason === "turn_end") {
4795
+ this.broadcastActivity(agentId, "working", "Message received");
4796
+ }
4060
4797
  if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
4061
4798
  return true;
4062
4799
  }
@@ -4233,6 +4970,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4233
4970
  this.broadcastActivity(agentId, "online", "Idle");
4234
4971
  }
4235
4972
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
4973
+ if (ap.driver.terminateProcessOnTurnEnd) {
4974
+ ap.expectedTerminationReason = "turn_end";
4975
+ logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
4976
+ try {
4977
+ ap.process.kill("SIGTERM");
4978
+ } catch (err) {
4979
+ const reason = err instanceof Error ? err.message : String(err);
4980
+ logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
4981
+ }
4982
+ }
4236
4983
  }
4237
4984
  if (event.sessionId) {
4238
4985
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
@@ -4294,7 +5041,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4294
5041
  if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
4295
5042
  const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
4296
5043
  console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
4297
- this.broadcastActivity(agentId, "working", "Message received");
4298
5044
  if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
4299
5045
  return;
4300
5046
  }
@@ -4311,15 +5057,15 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4311
5057
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
4312
5058
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
4313
5059
  if (messages.length === 0) return true;
4314
- const prompt = messages.length === 1 ? `New message received:
5060
+ const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
4315
5061
 
4316
- ${formatIncomingMessage(messages[0])}
5062
+ ${formatIncomingMessage(messages[0], ap.driver)}
4317
5063
 
4318
5064
  Respond as appropriate. Complete all your work before stopping.` : `New messages received:
4319
5065
 
4320
- ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
5066
+ ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
4321
5067
 
4322
- Respond as appropriate. Complete all your work before stopping.`;
5068
+ Respond as appropriate. Complete all your work before stopping.`);
4323
5069
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
4324
5070
  if (!encoded) {
4325
5071
  ap.inbox.unshift(...messages);
@@ -4355,8 +5101,8 @@ Respond as appropriate. Complete all your work before stopping.`;
4355
5101
  const nodes = [];
4356
5102
  for (const entry of entries) {
4357
5103
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
4358
- const fullPath = path10.join(dir, entry.name);
4359
- const relativePath = path10.relative(rootDir, fullPath);
5104
+ const fullPath = path11.join(dir, entry.name);
5105
+ const relativePath = path11.relative(rootDir, fullPath);
4360
5106
  let info;
4361
5107
  try {
4362
5108
  info = await stat2(fullPath);
@@ -4587,10 +5333,10 @@ var ReminderCache = class {
4587
5333
 
4588
5334
  // src/machineLock.ts
4589
5335
  import { createHash, randomUUID as randomUUID2 } from "crypto";
4590
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync, writeFileSync as writeFileSync7 } from "fs";
4591
- import os4 from "os";
4592
- import path11 from "path";
4593
- var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
5336
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
5337
+ import os5 from "os";
5338
+ import path12 from "path";
5339
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
4594
5340
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
4595
5341
  var DaemonMachineLockConflictError = class extends Error {
4596
5342
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -4609,18 +5355,18 @@ function getDaemonMachineLockId(apiKey) {
4609
5355
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
4610
5356
  }
4611
5357
  function ownerPath(lockDir) {
4612
- return path11.join(lockDir, "owner.json");
5358
+ return path12.join(lockDir, "owner.json");
4613
5359
  }
4614
5360
  function readOwner(lockDir) {
4615
5361
  try {
4616
- return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
5362
+ return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
4617
5363
  } catch {
4618
5364
  return null;
4619
5365
  }
4620
5366
  }
4621
5367
  function lockAgeMs(lockDir) {
4622
5368
  try {
4623
- return Date.now() - statSync(lockDir).mtimeMs;
5369
+ return Date.now() - statSync2(lockDir).mtimeMs;
4624
5370
  } catch {
4625
5371
  return null;
4626
5372
  }
@@ -4639,26 +5385,26 @@ function acquireDaemonMachineLock(options) {
4639
5385
  const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
4640
5386
  const fingerprint = apiKeyFingerprint(options.apiKey);
4641
5387
  const lockId = getDaemonMachineLockId(options.apiKey);
4642
- const machineDir = path11.join(rootDir, lockId);
4643
- const lockDir = path11.join(machineDir, "daemon.lock");
5388
+ const machineDir = path12.join(rootDir, lockId);
5389
+ const lockDir = path12.join(machineDir, "daemon.lock");
4644
5390
  const token = randomUUID2();
4645
- mkdirSync4(machineDir, { recursive: true });
5391
+ mkdirSync5(machineDir, { recursive: true });
4646
5392
  for (let attempt = 0; attempt < 2; attempt += 1) {
4647
5393
  try {
4648
- mkdirSync4(lockDir);
5394
+ mkdirSync5(lockDir);
4649
5395
  const owner = {
4650
5396
  pid: process.pid,
4651
5397
  token,
4652
- hostname: os4.hostname(),
5398
+ hostname: os5.hostname(),
4653
5399
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4654
5400
  serverUrl: options.serverUrl,
4655
5401
  apiKeyFingerprint: fingerprint.slice(0, 16)
4656
5402
  };
4657
5403
  try {
4658
- writeFileSync7(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
5404
+ writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
4659
5405
  `, { mode: 384 });
4660
5406
  } catch (err) {
4661
- rmSync(lockDir, { recursive: true, force: true });
5407
+ rmSync2(lockDir, { recursive: true, force: true });
4662
5408
  throw err;
4663
5409
  }
4664
5410
  return {
@@ -4668,7 +5414,7 @@ function acquireDaemonMachineLock(options) {
4668
5414
  release: () => {
4669
5415
  const currentOwner = readOwner(lockDir);
4670
5416
  if (currentOwner?.pid === process.pid && currentOwner.token === token) {
4671
- rmSync(lockDir, { recursive: true, force: true });
5417
+ rmSync2(lockDir, { recursive: true, force: true });
4672
5418
  }
4673
5419
  }
4674
5420
  };
@@ -4685,7 +5431,7 @@ function acquireDaemonMachineLock(options) {
4685
5431
  throw new DaemonMachineLockConflictError(lockDir, null);
4686
5432
  }
4687
5433
  }
4688
- rmSync(lockDir, { recursive: true, force: true });
5434
+ rmSync2(lockDir, { recursive: true, force: true });
4689
5435
  }
4690
5436
  }
4691
5437
  throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
@@ -4712,23 +5458,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
4712
5458
  }
4713
5459
  }
4714
5460
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
4715
- const dirname = path12.dirname(fileURLToPath(moduleUrl));
4716
- const jsPath = path12.resolve(dirname, "chat-bridge.js");
5461
+ const dirname = path13.dirname(fileURLToPath(moduleUrl));
5462
+ const jsPath = path13.resolve(dirname, "chat-bridge.js");
4717
5463
  try {
4718
5464
  accessSync(jsPath);
4719
5465
  return jsPath;
4720
5466
  } catch {
4721
- return path12.resolve(dirname, "chat-bridge.ts");
5467
+ return path13.resolve(dirname, "chat-bridge.ts");
4722
5468
  }
4723
5469
  }
4724
5470
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
4725
- const thisDir = path12.dirname(fileURLToPath(moduleUrl));
4726
- const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
5471
+ const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5472
+ const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
4727
5473
  try {
4728
5474
  accessSync(bundledDistPath);
4729
5475
  return bundledDistPath;
4730
5476
  } catch {
4731
- const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5477
+ const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
4732
5478
  accessSync(workspaceDistPath);
4733
5479
  return workspaceDistPath;
4734
5480
  }
@@ -4737,9 +5483,14 @@ function detectRuntimes() {
4737
5483
  const ids = [];
4738
5484
  const versions = {};
4739
5485
  for (const runtime of RUNTIMES) {
5486
+ const driver = getDriver(runtime.id);
4740
5487
  try {
4741
- const probe = getDriver(runtime.id).probe?.();
4742
- if (probe?.available) {
5488
+ if (driver.probe) {
5489
+ const probe = driver.probe();
5490
+ if (!probe.available) {
5491
+ if (probe.version) versions[runtime.id] = probe.version;
5492
+ continue;
5493
+ }
4743
5494
  ids.push(runtime.id);
4744
5495
  if (probe.version) versions[runtime.id] = probe.version;
4745
5496
  continue;
@@ -4840,7 +5591,7 @@ var DaemonCore = class {
4840
5591
  }
4841
5592
  resolveMachineStateRoot() {
4842
5593
  if (this.options.machineStateDir) return this.options.machineStateDir;
4843
- if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
5594
+ if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
4844
5595
  return DEFAULT_MACHINE_STATE_ROOT;
4845
5596
  }
4846
5597
  start() {
@@ -5031,8 +5782,8 @@ var DaemonCore = class {
5031
5782
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
5032
5783
  runtimes,
5033
5784
  runningAgents: this.agentManager.getRunningAgentIds(),
5034
- hostname: this.options.hostname ?? os5.hostname(),
5035
- os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
5785
+ hostname: this.options.hostname ?? os6.hostname(),
5786
+ os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
5036
5787
  daemonVersion: this.daemonVersion
5037
5788
  });
5038
5789
  for (const agentId of this.agentManager.getRunningAgentIds()) {