@slock-ai/daemon 0.42.0 → 0.44.2

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",
@@ -584,8 +633,8 @@ var DISPLAY_PLAN_CONFIG = {
584
633
  // src/agentProcessManager.ts
585
634
  import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
586
635
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
587
- import path10 from "path";
588
- import os3 from "os";
636
+ import path11 from "path";
637
+ import os4 from "os";
589
638
 
590
639
  // src/drivers/claude.ts
591
640
  import { spawn } from "child_process";
@@ -631,6 +680,9 @@ function buildPrompt(config, variant, opts) {
631
680
  const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
632
681
  const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
633
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")}\``;
634
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.";
635
687
  const criticalRules = isCli ? [
636
688
  "- Always communicate through `slock` CLI commands. This is your only output channel.",
@@ -681,15 +733,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
681
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\`.
682
734
  15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
683
735
  16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
684
- 17. **\`slock profile update\`** \u2014 Update your own profile. Currently this only supports \`--avatar-file\`.
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.
685
737
  18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
686
738
  19. **\`slock reminder list\`** \u2014 List your reminders.
687
739
  20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
688
740
 
689
- When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
690
- Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
691
- 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.
692
-
693
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:
694
742
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
695
743
 
@@ -712,7 +760,15 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
712
760
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
713
761
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
714
762
  12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
715
- 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.`;
716
772
  const sendingMessagesSection = isCli ? `### Sending messages
717
773
 
718
774
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
@@ -782,6 +838,11 @@ To jump directly to a specific hit with nearby context, use \`slock message read
782
838
  Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
783
839
 
784
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.`;
785
846
  const tasksSection = isCli ? `### Tasks
786
847
 
787
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.
@@ -931,6 +992,8 @@ Header fields:
931
992
 
932
993
  ${sendingMessagesSection}
933
994
 
995
+ ${reminderSection}
996
+
934
997
  ${threadsSection}
935
998
 
936
999
  ${discoverySection}
@@ -939,6 +1002,8 @@ ${channelAwarenessSection}
939
1002
 
940
1003
  ${readingHistorySection}
941
1004
 
1005
+ ${historicalReferenceSection}
1006
+
942
1007
  ${tasksSection}
943
1008
 
944
1009
  ### Splitting tasks for parallel execution
@@ -996,7 +1061,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
996
1061
 
997
1062
  ## Workspace & Memory
998
1063
 
999
- 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.
1000
1065
 
1001
1066
  ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1002
1067
 
@@ -1263,8 +1328,25 @@ function probeClaude(deps = {}) {
1263
1328
  }
1264
1329
  var ClaudeDriver = class {
1265
1330
  id = "claude";
1331
+ lifecycle = {
1332
+ kind: "persistent",
1333
+ stdin: "gated",
1334
+ inFlightWake: "queue"
1335
+ };
1336
+ communication = {
1337
+ chat: "slock_cli",
1338
+ runtimeControl: "mcp_runtime_actions"
1339
+ };
1340
+ session = {
1341
+ recovery: "resume_or_fresh"
1342
+ };
1343
+ model = {
1344
+ detectedModelsVerifiedAs: "launchable",
1345
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
1346
+ };
1266
1347
  supportsStdinNotification = true;
1267
1348
  mcpToolPrefix = "mcp__chat__";
1349
+ usesSlockCliForCommunication = true;
1268
1350
  // Claude Code supports same-turn steering, but raw stdin injection at an
1269
1351
  // arbitrary busy instant can collide with active signed thinking blocks. The
1270
1352
  // daemon therefore gates busy delivery on Claude stream-json boundaries.
@@ -1566,8 +1648,25 @@ function joinReasoningText(item) {
1566
1648
  }
1567
1649
  var CodexDriver = class {
1568
1650
  id = "codex";
1651
+ lifecycle = {
1652
+ kind: "persistent",
1653
+ stdin: "direct",
1654
+ inFlightWake: "steer"
1655
+ };
1656
+ communication = {
1657
+ chat: "slock_cli",
1658
+ runtimeControl: "mcp_runtime_actions"
1659
+ };
1660
+ session = {
1661
+ recovery: "resume_or_fresh"
1662
+ };
1663
+ model = {
1664
+ detectedModelsVerifiedAs: "launchable",
1665
+ toLaunchSpec: (modelId) => ({ params: { model: modelId } })
1666
+ };
1569
1667
  supportsStdinNotification = true;
1570
1668
  mcpToolPrefix = "mcp_chat_";
1669
+ usesSlockCliForCommunication = true;
1571
1670
  busyDeliveryMode = "direct";
1572
1671
  supportsNativeStandingPrompt = true;
1573
1672
  probe() {
@@ -1962,7 +2061,7 @@ function detectCodexModels(home = os.homedir()) {
1962
2061
  if (entry?.visibility && entry.visibility !== "public") continue;
1963
2062
  if (entry?.supported_in_api === false) continue;
1964
2063
  const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
1965
- models.push({ id: slug, label });
2064
+ models.push({ id: slug, label, verified: "launchable" });
1966
2065
  }
1967
2066
  } catch {
1968
2067
  return null;
@@ -1984,6 +2083,23 @@ import path5 from "path";
1984
2083
  import { writeFileSync as writeFileSync3 } from "fs";
1985
2084
  var CopilotDriver = class {
1986
2085
  id = "copilot";
2086
+ lifecycle = {
2087
+ kind: "per_turn",
2088
+ start: "immediate",
2089
+ exit: "natural",
2090
+ inFlightWake: "spawn_new"
2091
+ };
2092
+ communication = {
2093
+ chat: "mcp_chat_bridge",
2094
+ runtimeControl: "mcp_runtime_actions"
2095
+ };
2096
+ session = {
2097
+ recovery: "resume_or_fresh"
2098
+ };
2099
+ model = {
2100
+ detectedModelsVerifiedAs: "launchable",
2101
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2102
+ };
1987
2103
  supportsStdinNotification = false;
1988
2104
  mcpToolPrefix = "";
1989
2105
  busyDeliveryMode = "none";
@@ -2117,11 +2233,28 @@ var CopilotDriver = class {
2117
2233
  };
2118
2234
 
2119
2235
  // src/drivers/cursor.ts
2120
- import { spawn as spawn4 } from "child_process";
2236
+ import { spawn as spawn4, spawnSync } from "child_process";
2121
2237
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2122
2238
  import path6 from "path";
2123
2239
  var CursorDriver = class {
2124
2240
  id = "cursor";
2241
+ lifecycle = {
2242
+ kind: "per_turn",
2243
+ start: "immediate",
2244
+ exit: "natural",
2245
+ inFlightWake: "spawn_new"
2246
+ };
2247
+ communication = {
2248
+ chat: "mcp_chat_bridge",
2249
+ runtimeControl: "mcp_runtime_actions"
2250
+ };
2251
+ session = {
2252
+ recovery: "resume_or_fresh"
2253
+ };
2254
+ model = {
2255
+ detectedModelsVerifiedAs: "launchable",
2256
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2257
+ };
2125
2258
  supportsStdinNotification = false;
2126
2259
  mcpToolPrefix = "mcp__chat__";
2127
2260
  busyDeliveryMode = "none";
@@ -2234,14 +2367,83 @@ var CursorDriver = class {
2234
2367
  messageNotificationStyle: "poll"
2235
2368
  });
2236
2369
  }
2370
+ async detectModels() {
2371
+ return detectCursorModels();
2372
+ }
2237
2373
  };
2374
+ function parseCursorModelsOutput(output) {
2375
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
2376
+ const models = [];
2377
+ let defaultModel;
2378
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
2379
+ const line = rawLine.trim();
2380
+ if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
2381
+ if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
2382
+ let modelLine = line;
2383
+ const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
2384
+ const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
2385
+ if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
2386
+ const markerStart = markerMatch?.index ?? modelLine.length;
2387
+ modelLine = modelLine.slice(0, markerStart).trim();
2388
+ }
2389
+ const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
2390
+ if (!match) continue;
2391
+ const id = match[1]?.trim();
2392
+ if (!id || id.startsWith("-")) continue;
2393
+ const label = match[2]?.trim() || id;
2394
+ models.push({ id, label, verified: "launchable" });
2395
+ if (markers.includes("default")) defaultModel = id;
2396
+ }
2397
+ if (models.length === 0) return null;
2398
+ return { models, default: defaultModel };
2399
+ }
2400
+ function detectCursorModels(runCommand = runCursorModelsCommand) {
2401
+ const result = runCommand();
2402
+ if (result.error || result.status !== 0) return null;
2403
+ return parseCursorModelsOutput(String(result.stdout || ""));
2404
+ }
2405
+ function runCursorModelsCommand() {
2406
+ return spawnSync("cursor-agent", ["models"], {
2407
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
2408
+ encoding: "utf8",
2409
+ timeout: 5e3
2410
+ });
2411
+ }
2238
2412
 
2239
2413
  // src/drivers/gemini.ts
2240
2414
  import { spawn as spawn5 } from "child_process";
2241
2415
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2242
2416
  import path7 from "path";
2417
+ function buildGeminiSpawnEnv(ctx) {
2418
+ return {
2419
+ ...process.env,
2420
+ FORCE_COLOR: "0",
2421
+ NO_COLOR: "1",
2422
+ // Gemini CLI's trusted-workspace gate breaks our managed headless flow
2423
+ // unless we explicitly trust the daemon-owned agent workspace.
2424
+ GEMINI_CLI_TRUST_WORKSPACE: "true",
2425
+ ...ctx.config.envVars || {}
2426
+ };
2427
+ }
2243
2428
  var GeminiDriver = class {
2244
2429
  id = "gemini";
2430
+ lifecycle = {
2431
+ kind: "per_turn",
2432
+ start: "immediate",
2433
+ exit: "natural",
2434
+ inFlightWake: "spawn_new"
2435
+ };
2436
+ communication = {
2437
+ chat: "mcp_chat_bridge",
2438
+ runtimeControl: "mcp_runtime_actions"
2439
+ };
2440
+ session = {
2441
+ recovery: "resume_or_fresh"
2442
+ };
2443
+ model = {
2444
+ detectedModelsVerifiedAs: "launchable",
2445
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2446
+ };
2245
2447
  supportsStdinNotification = false;
2246
2448
  mcpToolPrefix = "";
2247
2449
  busyDeliveryMode = "none";
@@ -2279,7 +2481,7 @@ var GeminiDriver = class {
2279
2481
  if (ctx.config.sessionId) {
2280
2482
  args.push("--resume", ctx.config.sessionId);
2281
2483
  }
2282
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2484
+ const spawnEnv = buildGeminiSpawnEnv(ctx);
2283
2485
  const proc = spawn5("gemini", args, {
2284
2486
  cwd: ctx.workingDirectory,
2285
2487
  stdio: ["pipe", "pipe", "pipe"],
@@ -2371,6 +2573,22 @@ function parseToolArguments(raw) {
2371
2573
  }
2372
2574
  var KimiDriver = class {
2373
2575
  id = "kimi";
2576
+ lifecycle = {
2577
+ kind: "persistent",
2578
+ stdin: "direct",
2579
+ inFlightWake: "steer"
2580
+ };
2581
+ communication = {
2582
+ chat: "mcp_chat_bridge",
2583
+ runtimeControl: "mcp_runtime_actions"
2584
+ };
2585
+ session = {
2586
+ recovery: "resume_or_fresh"
2587
+ };
2588
+ model = {
2589
+ detectedModelsVerifiedAs: "launchable",
2590
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2591
+ };
2374
2592
  supportsStdinNotification = true;
2375
2593
  mcpToolPrefix = "";
2376
2594
  busyDeliveryMode = "direct";
@@ -2571,7 +2789,7 @@ function detectKimiModels(home = os2.homedir()) {
2571
2789
  let key = match[1].trim();
2572
2790
  if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
2573
2791
  if (!key) continue;
2574
- models.push({ id: key, label: key });
2792
+ models.push({ id: key, label: key, verified: "launchable" });
2575
2793
  }
2576
2794
  void sectionRe;
2577
2795
  if (models.length === 0) return null;
@@ -2581,6 +2799,314 @@ function detectKimiModels(home = os2.homedir()) {
2581
2799
  return { models, default: defaultModel };
2582
2800
  }
2583
2801
 
2802
+ // src/drivers/opencode.ts
2803
+ import { spawn as spawn7 } from "child_process";
2804
+ import { readFileSync as readFileSync3 } from "fs";
2805
+ import os3 from "os";
2806
+ import path9 from "path";
2807
+ var CHAT_MCP_SERVER_NAME = "chat";
2808
+ var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
2809
+ var SLOCK_AGENT_NAME = "slock";
2810
+ var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2811
+ var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2812
+ var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
2813
+ function buildChatBridgeCommand(ctx) {
2814
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2815
+ return [
2816
+ isTsSource ? "npx" : "node",
2817
+ ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
2818
+ "--agent-id",
2819
+ ctx.agentId,
2820
+ "--server-url",
2821
+ ctx.config.serverUrl,
2822
+ "--auth-token",
2823
+ ctx.config.authToken || ctx.daemonApiKey,
2824
+ "--runtime",
2825
+ "opencode",
2826
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2827
+ "--runtime-actions-only"
2828
+ ];
2829
+ }
2830
+ function parseOpenCodeConfigContent(raw) {
2831
+ if (!raw) return {};
2832
+ try {
2833
+ const parsed = JSON.parse(raw);
2834
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2835
+ return parsed;
2836
+ }
2837
+ } catch {
2838
+ }
2839
+ return {};
2840
+ }
2841
+ function parseUserOpenCodeConfig(ctx) {
2842
+ const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2843
+ return parseOpenCodeConfigContent(raw);
2844
+ }
2845
+ function readLocalOpenCodeConfig(home = os3.homedir()) {
2846
+ const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2847
+ try {
2848
+ return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
2849
+ } catch {
2850
+ }
2851
+ return {};
2852
+ }
2853
+ function recordField(value) {
2854
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2855
+ }
2856
+ function parseSemver(version) {
2857
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
2858
+ if (!match) return null;
2859
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
2860
+ }
2861
+ function isSupportedOpenCodeVersion(version) {
2862
+ if (!version) return true;
2863
+ const actual = parseSemver(version);
2864
+ const minimum = parseSemver(MIN_SUPPORTED_OPENCODE_VERSION);
2865
+ if (!actual || !minimum) return true;
2866
+ for (let i = 0; i < 3; i += 1) {
2867
+ if (actual[i] > minimum[i]) return true;
2868
+ if (actual[i] < minimum[i]) return false;
2869
+ }
2870
+ return true;
2871
+ }
2872
+ function unsupportedOpenCodeVersionMessage(version) {
2873
+ if (!version || isSupportedOpenCodeVersion(version)) return null;
2874
+ return `OpenCode CLI ${version} is unsupported; requires OpenCode >= ${MIN_SUPPORTED_OPENCODE_VERSION}. Upgrade opencode before starting this runtime.`;
2875
+ }
2876
+ function mergeOpenCodeConfigs(localConfig, envConfig) {
2877
+ return {
2878
+ ...localConfig,
2879
+ ...envConfig,
2880
+ provider: {
2881
+ ...recordField(localConfig.provider),
2882
+ ...recordField(envConfig.provider)
2883
+ },
2884
+ agent: {
2885
+ ...recordField(localConfig.agent),
2886
+ ...recordField(envConfig.agent)
2887
+ },
2888
+ mcp: {
2889
+ ...recordField(localConfig.mcp),
2890
+ ...recordField(envConfig.mcp)
2891
+ }
2892
+ };
2893
+ }
2894
+ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2895
+ const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2896
+ const userAgents = recordField(userConfig.agent);
2897
+ const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
2898
+ return {
2899
+ ...userConfig,
2900
+ $schema: "https://opencode.ai/config.json",
2901
+ agent: {
2902
+ ...userAgents,
2903
+ [SLOCK_AGENT_NAME]: {
2904
+ ...userSlockAgent,
2905
+ description: "Slock agent runtime",
2906
+ prompt: ctx.standingPrompt
2907
+ }
2908
+ },
2909
+ mcp: {
2910
+ ...recordField(userConfig.mcp),
2911
+ [CHAT_MCP_SERVER_NAME]: {
2912
+ type: "local",
2913
+ command: buildChatBridgeCommand(ctx),
2914
+ enabled: true
2915
+ }
2916
+ }
2917
+ };
2918
+ }
2919
+ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2920
+ const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2921
+ const config = buildOpenCodeConfig(ctx, home);
2922
+ const env = {
2923
+ ...slock.spawnEnv,
2924
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config)
2925
+ };
2926
+ const args = [
2927
+ "run",
2928
+ "--format",
2929
+ "json",
2930
+ "--dangerously-skip-permissions",
2931
+ "--pure",
2932
+ "--dir",
2933
+ ctx.workingDirectory
2934
+ ];
2935
+ if (ctx.config.model && ctx.config.model !== "default") {
2936
+ args.push("--model", ctx.config.model);
2937
+ }
2938
+ args.push("--agent", SLOCK_AGENT_NAME);
2939
+ if (ctx.config.sessionId) {
2940
+ args.push("--session", ctx.config.sessionId);
2941
+ }
2942
+ const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT : ctx.prompt;
2943
+ args.push("--", turnPrompt);
2944
+ return { args, env, config };
2945
+ }
2946
+ function detectOpenCodeModels(home = os3.homedir()) {
2947
+ const models = (RUNTIME_MODELS.opencode || []).map((model) => ({
2948
+ ...model,
2949
+ verified: "suggestion_only"
2950
+ }));
2951
+ const providers = recordField(readLocalOpenCodeConfig(home).provider);
2952
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
2953
+ const providerModels = recordField(recordField(providerConfig).models);
2954
+ for (const [modelId, modelConfig] of Object.entries(providerModels)) {
2955
+ const fullId = `${providerId}/${modelId}`;
2956
+ if (models.some((model2) => model2.id === fullId)) continue;
2957
+ const model = recordField(modelConfig);
2958
+ const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
2959
+ models.push({ id: fullId, label: name, verified: "launchable" });
2960
+ }
2961
+ }
2962
+ return models.length > 0 ? { models } : null;
2963
+ }
2964
+ function isSystemFirstMessageTask(message) {
2965
+ return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2966
+ }
2967
+ function buildOpenCodeSystemPrompt(config) {
2968
+ return buildCliTransportSystemPrompt(config, {
2969
+ toolPrefix: CHAT_MCP_TOOL_PREFIX,
2970
+ extraCriticalRules: [
2971
+ "- 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."
2972
+ ],
2973
+ postStartupNotes: [
2974
+ "**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."
2975
+ ],
2976
+ includeStdinNotificationSection: false,
2977
+ messageNotificationStyle: "poll"
2978
+ });
2979
+ }
2980
+ var OpenCodeDriver = class {
2981
+ id = "opencode";
2982
+ lifecycle = {
2983
+ kind: "per_turn",
2984
+ start: "defer_until_concrete_message",
2985
+ exit: "terminate_on_turn_end",
2986
+ inFlightWake: "coalesce_into_pending"
2987
+ };
2988
+ communication = {
2989
+ chat: "slock_cli",
2990
+ runtimeControl: "mcp_runtime_actions"
2991
+ };
2992
+ session = {
2993
+ recovery: "resume_or_fresh"
2994
+ };
2995
+ model = {
2996
+ detectedModelsVerifiedAs: "launchable",
2997
+ toLaunchSpec: (modelId, ctx, opts) => {
2998
+ if (!ctx) return { args: ["--model", modelId] };
2999
+ const launchCtx = {
3000
+ ...ctx,
3001
+ config: {
3002
+ ...ctx.config,
3003
+ model: modelId
3004
+ }
3005
+ };
3006
+ const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
3007
+ return {
3008
+ args: launch.args,
3009
+ env: launch.env,
3010
+ config: launch.config
3011
+ };
3012
+ }
3013
+ };
3014
+ supportsStdinNotification = false;
3015
+ mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
3016
+ busyDeliveryMode = "none";
3017
+ terminateProcessOnTurnEnd = true;
3018
+ deferSpawnUntilMessage = true;
3019
+ usesSlockCliForCommunication = true;
3020
+ shouldDeferWakeMessage(message) {
3021
+ return isSystemFirstMessageTask(message);
3022
+ }
3023
+ sessionId = null;
3024
+ sessionAnnounced = false;
3025
+ probe() {
3026
+ if (!resolveCommandOnPath("opencode")) return { available: false };
3027
+ const version = readCommandVersion("opencode") || void 0;
3028
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
3029
+ if (unsupportedMessage) {
3030
+ return {
3031
+ available: false,
3032
+ version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
3033
+ };
3034
+ }
3035
+ return { available: true, version };
3036
+ }
3037
+ async detectModels() {
3038
+ return detectOpenCodeModels();
3039
+ }
3040
+ spawn(ctx) {
3041
+ this.sessionId = ctx.config.sessionId || null;
3042
+ this.sessionAnnounced = false;
3043
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(readCommandVersion("opencode"));
3044
+ if (unsupportedMessage) {
3045
+ throw new Error(unsupportedMessage);
3046
+ }
3047
+ const launch = buildOpenCodeLaunchOptions(ctx);
3048
+ const proc = spawn7("opencode", launch.args, {
3049
+ cwd: ctx.workingDirectory,
3050
+ stdio: ["pipe", "pipe", "pipe"],
3051
+ env: launch.env,
3052
+ shell: process.platform === "win32"
3053
+ });
3054
+ proc.stdin?.end();
3055
+ return { process: proc };
3056
+ }
3057
+ parseLine(line) {
3058
+ let event;
3059
+ try {
3060
+ event = JSON.parse(line);
3061
+ } catch {
3062
+ return [];
3063
+ }
3064
+ const events = [];
3065
+ if (event.sessionID && event.sessionID !== this.sessionId) {
3066
+ this.sessionId = event.sessionID;
3067
+ }
3068
+ if (!this.sessionAnnounced && this.sessionId) {
3069
+ events.push({ kind: "session_init", sessionId: this.sessionId });
3070
+ this.sessionAnnounced = true;
3071
+ }
3072
+ switch (event.type) {
3073
+ case "step_start":
3074
+ events.push({ kind: "thinking", text: "" });
3075
+ break;
3076
+ case "text":
3077
+ if (typeof event.part?.text === "string" && event.part.text.length > 0) {
3078
+ events.push({ kind: "text", text: event.part.text });
3079
+ }
3080
+ break;
3081
+ case "tool_use":
3082
+ events.push({
3083
+ kind: "tool_call",
3084
+ name: event.part?.tool || "unknown_tool",
3085
+ input: event.part?.state?.input
3086
+ });
3087
+ break;
3088
+ case "step_finish":
3089
+ if (event.part?.reason !== "tool-calls") {
3090
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
3091
+ }
3092
+ break;
3093
+ case "error": {
3094
+ const message = event.error?.data?.message || event.error?.message || (event.error?.name ? `${event.error.name} (no message)` : null) || "Unknown OpenCode error";
3095
+ events.push({ kind: "error", message });
3096
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
3097
+ break;
3098
+ }
3099
+ }
3100
+ return events;
3101
+ }
3102
+ encodeStdinMessage(_text, _sessionId, _opts) {
3103
+ return null;
3104
+ }
3105
+ buildSystemPrompt(config, _agentId) {
3106
+ return buildOpenCodeSystemPrompt(config);
3107
+ }
3108
+ };
3109
+
2584
3110
  // src/drivers/index.ts
2585
3111
  var driverFactories = {
2586
3112
  claude: () => new ClaudeDriver(),
@@ -2588,7 +3114,8 @@ var driverFactories = {
2588
3114
  copilot: () => new CopilotDriver(),
2589
3115
  cursor: () => new CursorDriver(),
2590
3116
  gemini: () => new GeminiDriver(),
2591
- kimi: () => new KimiDriver()
3117
+ kimi: () => new KimiDriver(),
3118
+ opencode: () => new OpenCodeDriver()
2592
3119
  };
2593
3120
  function getDriver(runtimeId) {
2594
3121
  const createDriver = driverFactories[runtimeId];
@@ -2601,7 +3128,7 @@ function getDriver(runtimeId) {
2601
3128
 
2602
3129
  // src/workspaces.ts
2603
3130
  import { readdir, rm, stat } from "fs/promises";
2604
- import path9 from "path";
3131
+ import path10 from "path";
2605
3132
  function isValidWorkspaceDirectoryName(directoryName) {
2606
3133
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
2607
3134
  }
@@ -2609,7 +3136,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
2609
3136
  if (!isValidWorkspaceDirectoryName(directoryName)) {
2610
3137
  return null;
2611
3138
  }
2612
- return path9.join(dataDir, directoryName);
3139
+ return path10.join(dataDir, directoryName);
2613
3140
  }
2614
3141
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
2615
3142
  return {
@@ -2658,7 +3185,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
2658
3185
  return summary;
2659
3186
  }
2660
3187
  const childSummaries = await Promise.all(
2661
- entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
3188
+ entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
2662
3189
  );
2663
3190
  for (const childSummary of childSummaries) {
2664
3191
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -2677,7 +3204,7 @@ async function scanWorkspaceDirectories(dataDir) {
2677
3204
  if (!entry.isDirectory()) {
2678
3205
  return null;
2679
3206
  }
2680
- const dirPath = path9.join(dataDir, entry.name);
3207
+ const dirPath = path10.join(dataDir, entry.name);
2681
3208
  try {
2682
3209
  const summary = await summarizeWorkspaceDirectory(dirPath);
2683
3210
  return {
@@ -2709,7 +3236,23 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
2709
3236
  }
2710
3237
 
2711
3238
  // src/agentProcessManager.ts
2712
- var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
3239
+ var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
3240
+ var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 1;
3241
+ var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3242
+ function readPositiveIntegerEnv(name, fallback) {
3243
+ const raw = process.env[name];
3244
+ if (!raw) return fallback;
3245
+ const parsed = Number(raw);
3246
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
3247
+ return Math.floor(parsed);
3248
+ }
3249
+ function readNonNegativeIntegerEnv(name, fallback) {
3250
+ const raw = process.env[name];
3251
+ if (!raw) return fallback;
3252
+ const parsed = Number(raw);
3253
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
3254
+ return Math.floor(parsed);
3255
+ }
2713
3256
  function toLocalTime(iso) {
2714
3257
  const d = new Date(iso);
2715
3258
  if (isNaN(d.getTime())) return iso;
@@ -2750,12 +3293,12 @@ function findSessionJsonl(root, predicate) {
2750
3293
  for (const entry of entries) {
2751
3294
  if (++visited > maxEntries) return null;
2752
3295
  if (!entry.isFile() || !predicate(entry.name)) continue;
2753
- return path10.join(dir, entry.name);
3296
+ return path11.join(dir, entry.name);
2754
3297
  }
2755
3298
  for (const entry of entries) {
2756
3299
  if (++visited > maxEntries) return null;
2757
3300
  if (!entry.isDirectory()) continue;
2758
- const found = visit(path10.join(dir, entry.name), depth - 1);
3301
+ const found = visit(path11.join(dir, entry.name), depth - 1);
2759
3302
  if (found) return found;
2760
3303
  }
2761
3304
  return null;
@@ -2768,9 +3311,9 @@ function safeSessionFilename(value) {
2768
3311
  }
2769
3312
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
2770
3313
  try {
2771
- const dir = path10.join(fallbackDir, ".slock", "runtime-sessions");
3314
+ const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
2772
3315
  mkdirSync4(dir, { recursive: true });
2773
- const filePath = path10.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3316
+ const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
2774
3317
  writeFileSync7(filePath, JSON.stringify({
2775
3318
  type: "runtime_session_handoff",
2776
3319
  runtime,
@@ -2789,8 +3332,8 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
2789
3332
  return null;
2790
3333
  }
2791
3334
  }
2792
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), fallbackDir) {
2793
- const directPath = path10.isAbsolute(sessionId) ? sessionId : null;
3335
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3336
+ const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
2794
3337
  if (directPath) {
2795
3338
  try {
2796
3339
  if (statSync(directPath).isFile()) {
@@ -2799,7 +3342,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), f
2799
3342
  } catch {
2800
3343
  }
2801
3344
  }
2802
- const resolvedPath = runtime === "claude" ? findSessionJsonl(path10.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path10.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
3345
+ 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;
2803
3346
  if (!resolvedPath && fallbackDir) {
2804
3347
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
2805
3348
  if (fallback) return fallback;
@@ -2830,12 +3373,38 @@ function formatThreadContextMessage(message) {
2830
3373
  const senderType = formatVisibleActorType(message.sender_type);
2831
3374
  return `- [msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}`;
2832
3375
  }
2833
- function formatIncomingMessage(message) {
3376
+ function mcpToolName(driver, name) {
3377
+ return `${driver?.mcpToolPrefix ?? ""}${name}`;
3378
+ }
3379
+ function communicationCommand(driver, name) {
3380
+ if (!driver?.usesSlockCliForCommunication) {
3381
+ return mcpToolName(driver, name);
3382
+ }
3383
+ switch (name) {
3384
+ case "send_message":
3385
+ return "slock message send";
3386
+ case "read_history":
3387
+ return "slock message read";
3388
+ case "check_messages":
3389
+ return "slock message check";
3390
+ case "claim_tasks":
3391
+ return "slock task claim";
3392
+ default:
3393
+ return `slock ${name.replace(/_/g, " ")}`;
3394
+ }
3395
+ }
3396
+ function dynamicReplyInstruction(driver) {
3397
+ return driver?.usesSlockCliForCommunication ? "reply using `slock message send --target <exact target>`" : `reply with ${communicationCommand(driver, "send_message")}`;
3398
+ }
3399
+ function dynamicClaimInstruction(driver) {
3400
+ return driver?.usesSlockCliForCommunication ? "claim the relevant task with `slock task claim`" : `claim the relevant task with ${communicationCommand(driver, "claim_tasks")}`;
3401
+ }
3402
+ function formatIncomingMessage(message, driver) {
2834
3403
  const threadJoinPrefix = message.thread_join_context ? [
2835
3404
  `[System: You were added to a new thread via @mention. Read this context before replying.]`,
2836
3405
  `parent: ${message.thread_join_context.parent_target}`,
2837
3406
  `thread: ${message.thread_join_context.thread_target}`,
2838
- `suggested next step: read_history(channel="${message.thread_join_context.suggested_read_history_target}")`,
3407
+ `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}")`}`,
2839
3408
  "",
2840
3409
  "Parent message:",
2841
3410
  formatThreadContextMessage(message.thread_join_context.parent_message),
@@ -3369,9 +3938,20 @@ function classifyTerminalFailure(ap) {
3369
3938
  return null;
3370
3939
  }
3371
3940
  function isMissingResumeSession(ap) {
3372
- if (ap.driver.id !== "claude") return false;
3373
3941
  if (!ap.sessionId) return false;
3374
- return /No conversation found with session ID/i.test(ap.lastRuntimeError || "");
3942
+ const candidates = [
3943
+ ap.lastRuntimeError,
3944
+ ...ap.recentStderr
3945
+ ].filter((value) => !!value);
3946
+ if (ap.driver.id === "claude") {
3947
+ return candidates.some((text) => /No conversation found with session ID/i.test(text));
3948
+ }
3949
+ if (ap.driver.id === "opencode") {
3950
+ return candidates.some(
3951
+ (text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
3952
+ );
3953
+ }
3954
+ return false;
3375
3955
  }
3376
3956
  function getMessageDeliveryText(driver) {
3377
3957
  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.";
@@ -3396,6 +3976,15 @@ var AgentProcessManager = class _AgentProcessManager {
3396
3976
  agents = /* @__PURE__ */ new Map();
3397
3977
  agentsStarting = /* @__PURE__ */ new Set();
3398
3978
  // Prevent concurrent starts of same agent
3979
+ queuedAgentStarts = /* @__PURE__ */ new Map();
3980
+ agentStartQueue = [];
3981
+ activeAgentStartPermits = /* @__PURE__ */ new Set();
3982
+ activeAgentStartCount = 0;
3983
+ agentStartPumpTimer = null;
3984
+ lastAgentStartAt = 0;
3985
+ lastAgentStartAgentId = null;
3986
+ maxConcurrentAgentStarts;
3987
+ agentStartIntervalMs;
3399
3988
  startingInboxes = /* @__PURE__ */ new Map();
3400
3989
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
3401
3990
  idleAgentConfigs = /* @__PURE__ */ new Map();
@@ -3416,12 +4005,141 @@ var AgentProcessManager = class _AgentProcessManager {
3416
4005
  this.daemonApiKey = daemonApiKey;
3417
4006
  this.serverUrl = opts.serverUrl;
3418
4007
  this.dataDir = opts.dataDir || DATA_DIR;
3419
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os3.homedir();
4008
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
3420
4009
  this.driverResolver = opts.driverResolver || getDriver;
3421
4010
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3422
4011
  this.tracer = opts.tracer ?? noopTracer;
4012
+ this.maxConcurrentAgentStarts = Math.max(
4013
+ 1,
4014
+ Math.floor(
4015
+ opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
4016
+ )
4017
+ );
4018
+ this.agentStartIntervalMs = Math.max(
4019
+ 0,
4020
+ Math.floor(
4021
+ opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
4022
+ )
4023
+ );
3423
4024
  }
3424
4025
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4026
+ if (this.agents.has(agentId)) {
4027
+ logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4028
+ return;
4029
+ }
4030
+ if (this.agentsStarting.has(agentId)) {
4031
+ logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4032
+ return;
4033
+ }
4034
+ if (this.queuedAgentStarts.has(agentId)) {
4035
+ logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
4036
+ return;
4037
+ }
4038
+ return new Promise((resolve, reject) => {
4039
+ const item = {
4040
+ agentId,
4041
+ config,
4042
+ wakeMessage,
4043
+ unreadSummary,
4044
+ resumePrompt,
4045
+ launchId,
4046
+ resolve,
4047
+ reject
4048
+ };
4049
+ this.agentStartQueue.push(item);
4050
+ this.queuedAgentStarts.set(agentId, item);
4051
+ logger.info(
4052
+ `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
4053
+ );
4054
+ this.pumpAgentStartQueue();
4055
+ });
4056
+ }
4057
+ pumpAgentStartQueue() {
4058
+ if (this.agentStartPumpTimer) return;
4059
+ if (this.agentStartQueue.length === 0) return;
4060
+ if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
4061
+ const next = this.agentStartQueue[0];
4062
+ const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
4063
+ const elapsed = Date.now() - this.lastAgentStartAt;
4064
+ const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
4065
+ if (waitMs > 0) {
4066
+ this.agentStartPumpTimer = setTimeout(() => {
4067
+ this.agentStartPumpTimer = null;
4068
+ this.pumpAgentStartQueue();
4069
+ }, waitMs);
4070
+ return;
4071
+ }
4072
+ const item = this.agentStartQueue.shift();
4073
+ if (!item) return;
4074
+ if (this.queuedAgentStarts.get(item.agentId) !== item) {
4075
+ this.pumpAgentStartQueue();
4076
+ return;
4077
+ }
4078
+ this.queuedAgentStarts.delete(item.agentId);
4079
+ if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
4080
+ logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
4081
+ item.resolve();
4082
+ this.pumpAgentStartQueue();
4083
+ return;
4084
+ }
4085
+ this.activeAgentStartCount++;
4086
+ this.activeAgentStartPermits.add(item.agentId);
4087
+ this.lastAgentStartAt = Date.now();
4088
+ this.lastAgentStartAgentId = item.agentId;
4089
+ logger.info(
4090
+ `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
4091
+ );
4092
+ this.startAgentNow(
4093
+ item.agentId,
4094
+ item.config,
4095
+ item.wakeMessage,
4096
+ item.unreadSummary,
4097
+ item.resumePrompt,
4098
+ item.launchId
4099
+ ).then(item.resolve, (err) => {
4100
+ this.releaseAgentStartPermit(item.agentId, "start failed");
4101
+ item.reject(err);
4102
+ });
4103
+ }
4104
+ releaseAgentStartPermit(agentId, reason) {
4105
+ if (!this.activeAgentStartPermits.delete(agentId)) return false;
4106
+ this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
4107
+ logger.info(
4108
+ `[Agent ${agentId}] Start permit released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4109
+ );
4110
+ this.pumpAgentStartQueue();
4111
+ return true;
4112
+ }
4113
+ cancelQueuedAgentStart(agentId, reason) {
4114
+ const item = this.queuedAgentStarts.get(agentId);
4115
+ if (!item) return false;
4116
+ this.queuedAgentStarts.delete(agentId);
4117
+ this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
4118
+ this.startingInboxes.delete(agentId);
4119
+ if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
4120
+ clearTimeout(this.agentStartPumpTimer);
4121
+ this.agentStartPumpTimer = null;
4122
+ }
4123
+ logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
4124
+ item.resolve();
4125
+ return true;
4126
+ }
4127
+ cancelAllQueuedAgentStarts(reason) {
4128
+ for (const item of this.agentStartQueue) {
4129
+ if (this.queuedAgentStarts.get(item.agentId) === item) {
4130
+ logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
4131
+ item.resolve();
4132
+ }
4133
+ }
4134
+ this.agentStartQueue = [];
4135
+ this.queuedAgentStarts.clear();
4136
+ this.startingInboxes.clear();
4137
+ if (this.agentStartPumpTimer) {
4138
+ clearTimeout(this.agentStartPumpTimer);
4139
+ this.agentStartPumpTimer = null;
4140
+ }
4141
+ }
4142
+ async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
3425
4143
  if (this.agents.has(agentId)) {
3426
4144
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
3427
4145
  return;
@@ -3433,26 +4151,26 @@ var AgentProcessManager = class _AgentProcessManager {
3433
4151
  this.agentsStarting.add(agentId);
3434
4152
  try {
3435
4153
  const driver = this.driverResolver(config.runtime || "claude");
3436
- const agentDataDir = path10.join(this.dataDir, agentId);
4154
+ const agentDataDir = path11.join(this.dataDir, agentId);
3437
4155
  await mkdir(agentDataDir, { recursive: true });
3438
4156
  const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
3439
- const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
4157
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
3440
4158
  try {
3441
4159
  await access(memoryMdPath);
3442
4160
  } catch {
3443
4161
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
3444
4162
  await writeFile(memoryMdPath, initialMemoryMd);
3445
4163
  }
3446
- const notesDir = path10.join(agentDataDir, "notes");
4164
+ const notesDir = path11.join(agentDataDir, "notes");
3447
4165
  await mkdir(notesDir, { recursive: true });
3448
4166
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
3449
4167
  const seedFiles = buildOnboardingSeedFiles();
3450
4168
  for (const { relativePath, content } of seedFiles) {
3451
- const fullPath = path10.join(agentDataDir, relativePath);
4169
+ const fullPath = path11.join(agentDataDir, relativePath);
3452
4170
  try {
3453
4171
  await access(fullPath);
3454
4172
  } catch {
3455
- await mkdir(path10.dirname(fullPath), { recursive: true });
4173
+ await mkdir(path11.dirname(fullPath), { recursive: true });
3456
4174
  await writeFile(fullPath, content);
3457
4175
  }
3458
4176
  }
@@ -3470,7 +4188,7 @@ var AgentProcessManager = class _AgentProcessManager {
3470
4188
  const channelLabel = formatChannelLabel(wakeMessage);
3471
4189
  prompt = runtimeProfileControlPrompt ?? `New message received:
3472
4190
 
3473
- ${formatIncomingMessage(wakeMessage)}`;
4191
+ ${formatIncomingMessage(wakeMessage, driver)}`;
3474
4192
  if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
3475
4193
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
3476
4194
  if (otherUnread.length > 0) {
@@ -3483,13 +4201,13 @@ You also have unread messages in other channels:`;
3483
4201
  }
3484
4202
  prompt += `
3485
4203
 
3486
- Use read_history to catch up, or respond to the message above first.`;
4204
+ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to the message above first.`;
3487
4205
  }
3488
4206
  }
3489
4207
  if (!runtimeProfileControlPrompt) {
3490
4208
  prompt += `
3491
4209
 
3492
- Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
4210
+ Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
3493
4211
 
3494
4212
  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)}`;
3495
4213
  prompt += getBusyDeliveryNote(driver);
@@ -3502,7 +4220,7 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
3502
4220
  }
3503
4221
  prompt += `
3504
4222
 
3505
- 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)}`;
4223
+ 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)}`;
3506
4224
  } else if (isResume) {
3507
4225
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
3508
4226
  prompt += getBusyDeliveryNote(driver);
@@ -3510,6 +4228,25 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3510
4228
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
3511
4229
  }
3512
4230
  const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
4231
+ const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
4232
+ if (canDeferEmptyStart) {
4233
+ const pendingMessages = this.startingInboxes.get(agentId) || [];
4234
+ this.startingInboxes.delete(agentId);
4235
+ this.agentsStarting.delete(agentId);
4236
+ this.idleAgentConfigs.set(agentId, {
4237
+ config: effectiveConfig,
4238
+ sessionId: effectiveConfig.sessionId || null,
4239
+ launchId: launchId || null
4240
+ });
4241
+ this.sendAgentStatus(agentId, "active", launchId || null);
4242
+ this.broadcastActivity(agentId, "online", "Process idle");
4243
+ logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
4244
+ this.releaseAgentStartPermit(agentId, "spawn deferred");
4245
+ for (const message of pendingMessages) {
4246
+ this.deliverMessage(agentId, message);
4247
+ }
4248
+ return;
4249
+ }
3513
4250
  const { process: proc } = driver.spawn({
3514
4251
  agentId,
3515
4252
  config: effectiveConfig,
@@ -3528,6 +4265,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3528
4265
  config: runtimeConfig,
3529
4266
  sessionId: runtimeConfig.sessionId || null,
3530
4267
  launchId: launchId || null,
4268
+ startupWakeMessage: wakeMessage,
4269
+ startupUnreadSummary: unreadSummary,
4270
+ startupResumePrompt: resumePrompt,
3531
4271
  isIdle: false,
3532
4272
  notificationTimer: null,
3533
4273
  pendingNotificationCount: 0,
@@ -3539,12 +4279,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3539
4279
  runtimeTraceSpan: null,
3540
4280
  lastActivity: "",
3541
4281
  lastActivityDetail: "",
4282
+ activityClientSeq: 0,
3542
4283
  recentStdout: [],
3543
4284
  recentStderr: [],
3544
4285
  lastRuntimeError: null,
3545
4286
  spawnError: null,
3546
4287
  exitCode: null,
3547
4288
  exitSignal: null,
4289
+ expectedTerminationReason: null,
3548
4290
  pendingTrajectory: null,
3549
4291
  gatedSteering: createGatedSteeringState()
3550
4292
  };
@@ -3603,6 +4345,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3603
4345
  if (this.agents.has(agentId)) {
3604
4346
  const ap = this.agents.get(agentId);
3605
4347
  if (ap.process !== proc) return;
4348
+ this.releaseAgentStartPermit(agentId, "process closed");
3606
4349
  if (ap.notificationTimer) {
3607
4350
  clearTimeout(ap.notificationTimer);
3608
4351
  }
@@ -3614,20 +4357,60 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3614
4357
  }
3615
4358
  const finalCode = ap.exitCode ?? code;
3616
4359
  const finalSignal = ap.exitSignal ?? signal;
3617
- const terminalFailureDetail = classifyTerminalFailure(ap);
3618
- this.endRuntimeTrace(ap, finalCode === 0 ? "ok" : "error", {
3619
- outcome: finalCode === 0 ? "process-exit" : "process-crash",
4360
+ const expectedTermination = Boolean(ap.expectedTerminationReason);
4361
+ const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
4362
+ const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
4363
+ const missingResumeSession = isMissingResumeSession(ap);
4364
+ const summary = summarizeCrash(finalCode, finalSignal);
4365
+ this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
4366
+ outcome: processEndedCleanly ? "process-exit" : "process-crash",
4367
+ expectedTerminationReason: ap.expectedTerminationReason || void 0,
3620
4368
  exitCode: finalCode,
3621
4369
  exitSignal: finalSignal
3622
4370
  });
3623
- if (finalCode === 0) {
4371
+ if (processEndedCleanly) {
3624
4372
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
3625
4373
  } else {
3626
4374
  this.clearCompactionWatchdog(ap);
3627
4375
  }
3628
4376
  this.agents.delete(agentId);
3629
- if (finalCode === 0) {
3630
- const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
4377
+ if (missingResumeSession) {
4378
+ const staleSessionId = ap.sessionId;
4379
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : "Claude";
4380
+ const restartConfig = { ...ap.config, sessionId: null };
4381
+ logger.warn(
4382
+ `[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
4383
+ );
4384
+ this.broadcastActivity(
4385
+ agentId,
4386
+ "working",
4387
+ `Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
4388
+ [{ kind: "text", text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
4389
+ );
4390
+ this.startAgent(
4391
+ agentId,
4392
+ restartConfig,
4393
+ ap.startupWakeMessage,
4394
+ ap.startupUnreadSummary,
4395
+ ap.startupResumePrompt,
4396
+ ap.launchId || void 0
4397
+ ).catch((err) => {
4398
+ logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
4399
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
4400
+ this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
4401
+ });
4402
+ return;
4403
+ }
4404
+ if (processEndedCleanly) {
4405
+ let queuedWakeMessage;
4406
+ if (!ap.driver.supportsStdinNotification) {
4407
+ while (ap.inbox.length > 0) {
4408
+ const candidate = ap.inbox.shift();
4409
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
4410
+ queuedWakeMessage = candidate;
4411
+ break;
4412
+ }
4413
+ }
3631
4414
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
3632
4415
  if (queuedWakeMessage) {
3633
4416
  logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
@@ -3662,26 +4445,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3662
4445
  } else {
3663
4446
  this.idleAgentConfigs.delete(agentId);
3664
4447
  const reason = formatCrashReason(finalCode, finalSignal, ap);
3665
- const summary = summarizeCrash(finalCode, finalSignal);
3666
- if (isMissingResumeSession(ap)) {
3667
- const staleSessionId = ap.sessionId;
3668
- const restartConfig = { ...ap.config, sessionId: null };
3669
- logger.warn(
3670
- `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
3671
- );
3672
- this.broadcastActivity(
3673
- agentId,
3674
- "working",
3675
- "Stored Claude session missing; cold-starting a new session\u2026",
3676
- [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
3677
- );
3678
- this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
3679
- logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
3680
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
3681
- this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
3682
- });
3683
- return;
3684
- }
3685
4448
  logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
3686
4449
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
3687
4450
  if (terminalFailureDetail) {
@@ -3747,6 +4510,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3747
4510
  return leftKeys.every((key) => left?.[key] === right?.[key]);
3748
4511
  }
3749
4512
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
4513
+ this.cancelQueuedAgentStart(agentId, "stop requested");
3750
4514
  this.idleAgentConfigs.delete(agentId);
3751
4515
  const ap = this.agents.get(agentId);
3752
4516
  if (!ap) {
@@ -3755,6 +4519,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3755
4519
  }
3756
4520
  return;
3757
4521
  }
4522
+ this.releaseAgentStartPermit(agentId, "stop requested");
3758
4523
  if (ap.notificationTimer) {
3759
4524
  clearTimeout(ap.notificationTimer);
3760
4525
  }
@@ -3796,7 +4561,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3796
4561
  deliverMessage(agentId, message) {
3797
4562
  const ap = this.agents.get(agentId);
3798
4563
  if (!ap) {
3799
- if (this.agentsStarting.has(agentId)) {
4564
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
3800
4565
  const pending = this.startingInboxes.get(agentId) || [];
3801
4566
  pending.push(message);
3802
4567
  this.startingInboxes.set(agentId, pending);
@@ -3804,6 +4569,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3804
4569
  }
3805
4570
  const cached = this.idleAgentConfigs.get(agentId);
3806
4571
  if (cached) {
4572
+ const driver = this.driverResolver(cached.config.runtime || "claude");
4573
+ if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4574
+ return;
4575
+ }
3807
4576
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
3808
4577
  this.idleAgentConfigs.delete(agentId);
3809
4578
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
@@ -3812,6 +4581,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3812
4581
  }
3813
4582
  return;
3814
4583
  }
4584
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4585
+ return;
4586
+ }
3815
4587
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
3816
4588
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
3817
4589
  nextMessages.push(message);
@@ -3840,7 +4612,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3840
4612
  }
3841
4613
  }
3842
4614
  async resetWorkspace(agentId) {
3843
- const agentDataDir = path10.join(this.dataDir, agentId);
4615
+ const agentDataDir = path11.join(this.dataDir, agentId);
3844
4616
  try {
3845
4617
  await rm2(agentDataDir, { recursive: true, force: true });
3846
4618
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -3849,6 +4621,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3849
4621
  }
3850
4622
  }
3851
4623
  async stopAll() {
4624
+ this.cancelAllQueuedAgentStarts("daemon shutdown");
3852
4625
  this.idleAgentConfigs.clear();
3853
4626
  const ids = [...this.agents.keys()];
3854
4627
  await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
@@ -3856,6 +4629,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3856
4629
  getRunningAgentIds() {
3857
4630
  return [...this.agents.keys()];
3858
4631
  }
4632
+ shouldDeferWakeMessage(agentId, driver, message) {
4633
+ if (!driver.shouldDeferWakeMessage?.(message)) return false;
4634
+ logger.info(`[Agent ${agentId}] Deferred non-concrete wake message for ${driver.id}`);
4635
+ return true;
4636
+ }
3859
4637
  getAgentSessionId(agentId) {
3860
4638
  return this.agents.get(agentId)?.sessionId ?? null;
3861
4639
  }
@@ -3870,7 +4648,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3870
4648
  return result;
3871
4649
  }
3872
4650
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
3873
- const workspacePath = path10.join(this.dataDir, agentId);
4651
+ const workspacePath = path11.join(this.dataDir, agentId);
3874
4652
  return {
3875
4653
  agentId,
3876
4654
  launchId,
@@ -4019,7 +4797,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4019
4797
  }
4020
4798
  // Workspace file browsing
4021
4799
  async getFileTree(agentId, dirPath) {
4022
- const agentDir = path10.join(this.dataDir, agentId);
4800
+ const agentDir = path11.join(this.dataDir, agentId);
4023
4801
  try {
4024
4802
  await stat2(agentDir);
4025
4803
  } catch {
@@ -4027,8 +4805,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4027
4805
  }
4028
4806
  let targetDir = agentDir;
4029
4807
  if (dirPath) {
4030
- const resolved = path10.resolve(agentDir, dirPath);
4031
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4808
+ const resolved = path11.resolve(agentDir, dirPath);
4809
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
4032
4810
  return [];
4033
4811
  }
4034
4812
  targetDir = resolved;
@@ -4036,9 +4814,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4036
4814
  return this.listDirectoryChildren(targetDir, agentDir);
4037
4815
  }
4038
4816
  async readFile(agentId, filePath) {
4039
- const agentDir = path10.join(this.dataDir, agentId);
4040
- const resolved = path10.resolve(agentDir, filePath);
4041
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4817
+ const agentDir = path11.join(this.dataDir, agentId);
4818
+ const resolved = path11.resolve(agentDir, filePath);
4819
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
4042
4820
  throw new Error("Access denied");
4043
4821
  }
4044
4822
  const info = await stat2(resolved);
@@ -4062,7 +4840,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4062
4840
  ".sh",
4063
4841
  ".py"
4064
4842
  ]);
4065
- const ext = path10.extname(resolved).toLowerCase();
4843
+ const ext = path11.extname(resolved).toLowerCase();
4066
4844
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4067
4845
  return { content: null, binary: true };
4068
4846
  }
@@ -4088,14 +4866,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4088
4866
  async listSkills(agentId, runtimeHint) {
4089
4867
  const agent = this.agents.get(agentId);
4090
4868
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4091
- const home = os3.homedir();
4092
- const workspaceDir = path10.join(this.dataDir, agentId);
4869
+ const home = os4.homedir();
4870
+ const workspaceDir = path11.join(this.dataDir, agentId);
4093
4871
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4094
4872
  const globalResults = await Promise.all(
4095
- paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
4873
+ paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
4096
4874
  );
4097
4875
  const workspaceResults = await Promise.all(
4098
- paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
4876
+ paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
4099
4877
  );
4100
4878
  const dedup = (skills) => {
4101
4879
  const seen = /* @__PURE__ */ new Set();
@@ -4124,7 +4902,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4124
4902
  const skills = [];
4125
4903
  for (const entry of entries) {
4126
4904
  if (entry.isDirectory() || entry.isSymbolicLink()) {
4127
- const skillMd = path10.join(dir, entry.name, "SKILL.md");
4905
+ const skillMd = path11.join(dir, entry.name, "SKILL.md");
4128
4906
  try {
4129
4907
  const content = await readFile(skillMd, "utf-8");
4130
4908
  const skill = this.parseSkillMd(entry.name, content);
@@ -4135,7 +4913,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4135
4913
  } else if (entry.name.endsWith(".md")) {
4136
4914
  const cmdName = entry.name.replace(/\.md$/, "");
4137
4915
  try {
4138
- const content = await readFile(path10.join(dir, entry.name), "utf-8");
4916
+ const content = await readFile(path11.join(dir, entry.name), "utf-8");
4139
4917
  const skill = this.parseSkillMd(cmdName, content);
4140
4918
  skill.sourcePath = dir;
4141
4919
  skills.push(skill);
@@ -4178,13 +4956,15 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4178
4956
  if (!hasToolStart) {
4179
4957
  entries.push({ kind: "status", activity, detail });
4180
4958
  }
4959
+ if (ap) ap.activityClientSeq += 1;
4181
4960
  this.sendToServer({
4182
4961
  type: "agent:activity",
4183
4962
  agentId,
4184
4963
  activity,
4185
4964
  detail,
4186
4965
  entries,
4187
- launchId: launchIdOverride || ap?.launchId || void 0
4966
+ launchId: launchIdOverride || ap?.launchId || void 0,
4967
+ clientSeq: ap?.activityClientSeq
4188
4968
  });
4189
4969
  if (ap) {
4190
4970
  ap.lastActivity = activity;
@@ -4196,12 +4976,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4196
4976
  this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
4197
4977
  activity: ap.lastActivity
4198
4978
  });
4979
+ ap.activityClientSeq += 1;
4199
4980
  this.sendToServer({
4200
4981
  type: "agent:activity",
4201
4982
  agentId,
4202
4983
  activity: ap.lastActivity,
4203
4984
  detail: ap.lastActivityDetail,
4204
- launchId: launchIdOverride || ap.launchId || void 0
4985
+ launchId: launchIdOverride || ap.launchId || void 0,
4986
+ clientSeq: ap.activityClientSeq
4205
4987
  });
4206
4988
  }, ACTIVITY_HEARTBEAT_MS);
4207
4989
  }
@@ -4213,6 +4995,42 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4213
4995
  }
4214
4996
  }
4215
4997
  }
4998
+ /**
4999
+ * Respond to a server-issued `agent:activity_probe`. Echoes the
5000
+ * agent's current `lastActivity` back through the existing
5001
+ * `agent:activity` upstream channel with the matching `probeId`.
5002
+ *
5003
+ * Why this exists: the server's stale-activity sweep used to
5004
+ * synthesize `online` whenever a transient state went 90s without
5005
+ * an update. That invented state without consulting ground truth
5006
+ * and produced "agent shows green/idle but is actually working" UI
5007
+ * staleness (#engineering:72283cf7 task #340 RCA).
5008
+ *
5009
+ * The new flow: server sends `agent:activity_probe` for stale
5010
+ * agents, daemon replies here with the *real* current activity, and
5011
+ * the server only falls back to synth-online if the probe times out
5012
+ * (5s). The body is intentionally minimal — no entries, no
5013
+ * heartbeat side-effects, no state mutation. We just echo what we
5014
+ * already know.
5015
+ *
5016
+ * If the agent is no longer running locally (`ap` undefined), we
5017
+ * report `offline` so the server stops believing the agent is busy.
5018
+ */
5019
+ respondToActivityProbe(agentId, probeId) {
5020
+ const ap = this.agents.get(agentId);
5021
+ const activity = ap?.lastActivity || "offline";
5022
+ const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
5023
+ if (ap) ap.activityClientSeq += 1;
5024
+ this.sendToServer({
5025
+ type: "agent:activity",
5026
+ agentId,
5027
+ activity,
5028
+ detail,
5029
+ launchId: ap?.launchId || void 0,
5030
+ probeId,
5031
+ clientSeq: ap?.activityClientSeq
5032
+ });
5033
+ }
4216
5034
  flushPendingTrajectory(agentId) {
4217
5035
  const ap = this.agents.get(agentId);
4218
5036
  const pending = ap?.pendingTrajectory;
@@ -4350,7 +5168,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4350
5168
  logger.info(
4351
5169
  `[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
4352
5170
  );
4353
- this.broadcastActivity(agentId, "working", "Message received");
5171
+ if (reason === "turn_end") {
5172
+ this.broadcastActivity(agentId, "working", "Message received");
5173
+ }
4354
5174
  if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
4355
5175
  return true;
4356
5176
  }
@@ -4502,6 +5322,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4502
5322
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
4503
5323
  this.flushPendingTrajectory(agentId);
4504
5324
  if (ap) {
5325
+ this.releaseAgentStartPermit(agentId, "initial turn ended");
4505
5326
  this.clearGatedInFlightBatch(agentId, ap, "turn_end");
4506
5327
  if (event.sessionId) ap.sessionId = event.sessionId;
4507
5328
  ap.gatedSteering.outstandingToolUses = 0;
@@ -4527,6 +5348,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4527
5348
  this.broadcastActivity(agentId, "online", "Idle");
4528
5349
  }
4529
5350
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
5351
+ if (ap.driver.terminateProcessOnTurnEnd) {
5352
+ ap.expectedTerminationReason = "turn_end";
5353
+ logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
5354
+ try {
5355
+ ap.process.kill("SIGTERM");
5356
+ } catch (err) {
5357
+ const reason = err instanceof Error ? err.message : String(err);
5358
+ logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
5359
+ }
5360
+ }
4530
5361
  }
4531
5362
  if (event.sessionId) {
4532
5363
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
@@ -4588,7 +5419,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4588
5419
  if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
4589
5420
  const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
4590
5421
  console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
4591
- this.broadcastActivity(agentId, "working", "Message received");
4592
5422
  if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
4593
5423
  return;
4594
5424
  }
@@ -4607,11 +5437,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4607
5437
  if (messages.length === 0) return true;
4608
5438
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
4609
5439
 
4610
- ${formatIncomingMessage(messages[0])}
5440
+ ${formatIncomingMessage(messages[0], ap.driver)}
4611
5441
 
4612
5442
  Respond as appropriate. Complete all your work before stopping.` : `New messages received:
4613
5443
 
4614
- ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
5444
+ ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
4615
5445
 
4616
5446
  Respond as appropriate. Complete all your work before stopping.`);
4617
5447
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
@@ -4649,8 +5479,8 @@ Respond as appropriate. Complete all your work before stopping.`);
4649
5479
  const nodes = [];
4650
5480
  for (const entry of entries) {
4651
5481
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
4652
- const fullPath = path10.join(dir, entry.name);
4653
- const relativePath = path10.relative(rootDir, fullPath);
5482
+ const fullPath = path11.join(dir, entry.name);
5483
+ const relativePath = path11.relative(rootDir, fullPath);
4654
5484
  let info;
4655
5485
  try {
4656
5486
  info = await stat2(fullPath);
@@ -4881,10 +5711,10 @@ var ReminderCache = class {
4881
5711
 
4882
5712
  // src/machineLock.ts
4883
5713
  import { createHash, randomUUID as randomUUID2 } from "crypto";
4884
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
4885
- import os4 from "os";
4886
- import path11 from "path";
4887
- var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
5714
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
5715
+ import os5 from "os";
5716
+ import path12 from "path";
5717
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
4888
5718
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
4889
5719
  var DaemonMachineLockConflictError = class extends Error {
4890
5720
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -4903,11 +5733,11 @@ function getDaemonMachineLockId(apiKey) {
4903
5733
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
4904
5734
  }
4905
5735
  function ownerPath(lockDir) {
4906
- return path11.join(lockDir, "owner.json");
5736
+ return path12.join(lockDir, "owner.json");
4907
5737
  }
4908
5738
  function readOwner(lockDir) {
4909
5739
  try {
4910
- return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
5740
+ return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
4911
5741
  } catch {
4912
5742
  return null;
4913
5743
  }
@@ -4933,8 +5763,8 @@ function acquireDaemonMachineLock(options) {
4933
5763
  const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
4934
5764
  const fingerprint = apiKeyFingerprint(options.apiKey);
4935
5765
  const lockId = getDaemonMachineLockId(options.apiKey);
4936
- const machineDir = path11.join(rootDir, lockId);
4937
- const lockDir = path11.join(machineDir, "daemon.lock");
5766
+ const machineDir = path12.join(rootDir, lockId);
5767
+ const lockDir = path12.join(machineDir, "daemon.lock");
4938
5768
  const token = randomUUID2();
4939
5769
  mkdirSync5(machineDir, { recursive: true });
4940
5770
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -4943,7 +5773,7 @@ function acquireDaemonMachineLock(options) {
4943
5773
  const owner = {
4944
5774
  pid: process.pid,
4945
5775
  token,
4946
- hostname: os4.hostname(),
5776
+ hostname: os5.hostname(),
4947
5777
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4948
5778
  serverUrl: options.serverUrl,
4949
5779
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5006,23 +5836,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5006
5836
  }
5007
5837
  }
5008
5838
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5009
- const dirname = path12.dirname(fileURLToPath(moduleUrl));
5010
- const jsPath = path12.resolve(dirname, "chat-bridge.js");
5839
+ const dirname = path13.dirname(fileURLToPath(moduleUrl));
5840
+ const jsPath = path13.resolve(dirname, "chat-bridge.js");
5011
5841
  try {
5012
5842
  accessSync(jsPath);
5013
5843
  return jsPath;
5014
5844
  } catch {
5015
- return path12.resolve(dirname, "chat-bridge.ts");
5845
+ return path13.resolve(dirname, "chat-bridge.ts");
5016
5846
  }
5017
5847
  }
5018
5848
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5019
- const thisDir = path12.dirname(fileURLToPath(moduleUrl));
5020
- const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
5849
+ const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5850
+ const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
5021
5851
  try {
5022
5852
  accessSync(bundledDistPath);
5023
5853
  return bundledDistPath;
5024
5854
  } catch {
5025
- const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5855
+ const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5026
5856
  accessSync(workspaceDistPath);
5027
5857
  return workspaceDistPath;
5028
5858
  }
@@ -5031,9 +5861,14 @@ function detectRuntimes() {
5031
5861
  const ids = [];
5032
5862
  const versions = {};
5033
5863
  for (const runtime of RUNTIMES) {
5864
+ const driver = getDriver(runtime.id);
5034
5865
  try {
5035
- const probe = getDriver(runtime.id).probe?.();
5036
- if (probe?.available) {
5866
+ if (driver.probe) {
5867
+ const probe = driver.probe();
5868
+ if (!probe.available) {
5869
+ if (probe.version) versions[runtime.id] = probe.version;
5870
+ continue;
5871
+ }
5037
5872
  ids.push(runtime.id);
5038
5873
  if (probe.version) versions[runtime.id] = probe.version;
5039
5874
  continue;
@@ -5077,6 +5912,8 @@ function summarizeIncomingMessage(msg) {
5077
5912
  return `(agent=${msg.agentId}, path=${msg.path})`;
5078
5913
  case "agent:skills:list":
5079
5914
  return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
5915
+ case "agent:activity_probe":
5916
+ return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
5080
5917
  case "machine:workspace:delete":
5081
5918
  return `(directory=${msg.directoryName})`;
5082
5919
  case "machine:runtime_models:detect":
@@ -5134,7 +5971,7 @@ var DaemonCore = class {
5134
5971
  }
5135
5972
  resolveMachineStateRoot() {
5136
5973
  if (this.options.machineStateDir) return this.options.machineStateDir;
5137
- if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
5974
+ if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
5138
5975
  return DEFAULT_MACHINE_STATE_ROOT;
5139
5976
  }
5140
5977
  start() {
@@ -5264,6 +6101,9 @@ var DaemonCore = class {
5264
6101
  this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
5265
6102
  });
5266
6103
  break;
6104
+ case "agent:activity_probe":
6105
+ this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
6106
+ break;
5267
6107
  case "machine:workspace:scan":
5268
6108
  logger.info("[Daemon] Scanning all workspace directories");
5269
6109
  this.agentManager.scanAllWorkspaces().then((directories) => {
@@ -5281,7 +6121,12 @@ var DaemonCore = class {
5281
6121
  const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
5282
6122
  Promise.resolve(detect).then((result) => {
5283
6123
  if (result) {
5284
- this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models: result.models, default: result.default });
6124
+ const verified = driver.model.detectedModelsVerifiedAs;
6125
+ const models = result.models.map((model) => ({
6126
+ ...model,
6127
+ verified: model.verified ?? verified
6128
+ }));
6129
+ this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
5285
6130
  } else {
5286
6131
  this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
5287
6132
  }
@@ -5325,8 +6170,8 @@ var DaemonCore = class {
5325
6170
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
5326
6171
  runtimes,
5327
6172
  runningAgents: this.agentManager.getRunningAgentIds(),
5328
- hostname: this.options.hostname ?? os5.hostname(),
5329
- os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
6173
+ hostname: this.options.hostname ?? os6.hostname(),
6174
+ os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
5330
6175
  daemonVersion: this.daemonVersion
5331
6176
  });
5332
6177
  for (const agentId of this.agentManager.getRunningAgentIds()) {