@slock-ai/daemon 0.48.0 → 0.49.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.
@@ -1,18 +1,14 @@
1
1
  import {
2
2
  DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
3
- SLOCK_HOME_ENV,
4
3
  buildWebSocketOptions,
5
4
  executeJsonRequest,
6
5
  executeResponseRequest,
7
- listLegacySlockStatePaths,
8
- logger,
9
- resolveSlockHome,
10
- resolveSlockHomePath
11
- } from "./chunk-B7XIMLOT.js";
6
+ logger
7
+ } from "./chunk-KNMCE6WB.js";
12
8
 
13
9
  // src/core.ts
14
- import path15 from "path";
15
- import os7 from "os";
10
+ import path16 from "path";
11
+ import os8 from "os";
16
12
  import { createRequire } from "module";
17
13
  import { accessSync } from "fs";
18
14
  import { fileURLToPath } from "url";
@@ -481,6 +477,8 @@ function resolveSlockCliInvocation(toolName, input) {
481
477
  return { toolName: "list_server", input: {} };
482
478
  case "channel members":
483
479
  return { toolName: "list_channel_members", input: { channel: rest[0] } };
480
+ case "channel join":
481
+ return { toolName: "join_channel", input: { target: readOptionValue(rest, "--target") } };
484
482
  case "channel leave":
485
483
  return { toolName: "leave_channel", input: { target: readOptionValue(rest, "--target") } };
486
484
  case "task list":
@@ -766,21 +764,21 @@ var DISPLAY_PLAN_CONFIG = {
766
764
  };
767
765
 
768
766
  // src/agentProcessManager.ts
769
- import { mkdirSync as mkdirSync4, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
767
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
770
768
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
771
769
  import { createHash as createHash2 } from "crypto";
772
- import path11 from "path";
773
- import os5 from "os";
770
+ import path12 from "path";
771
+ import os6 from "os";
774
772
 
775
773
  // src/drivers/claude.ts
776
774
  import { spawn } from "child_process";
777
- import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
778
- import os from "os";
779
- import path3 from "path";
775
+ import { existsSync as existsSync3, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
776
+ import os2 from "os";
777
+ import path4 from "path";
780
778
 
781
779
  // src/drivers/cliTransport.ts
782
780
  import { mkdirSync, writeFileSync } from "fs";
783
- import path from "path";
781
+ import path2 from "path";
784
782
 
785
783
  // src/drivers/systemPrompt.ts
786
784
  function toolRef(prefix, name) {
@@ -858,27 +856,28 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
858
856
  2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
859
857
  3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
860
858
  4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
861
- 5. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
862
- 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.
863
- 7. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
864
- 8. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
865
- 9. **\`slock message react\`** \u2014 Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like \u{1F440}, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
866
- 10. **\`slock task list\`** \u2014 View a channel's task board.
867
- 11. **\`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).
868
- 12. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
869
- 13. **\`slock task unclaim\`** \u2014 Release your claim on a task.
870
- 14. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
871
- 15. **\`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\`.
872
- 16. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
873
- 17. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
874
- 18. **\`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.
875
- 19. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
876
- 20. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
877
- 21. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
878
- 22. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
879
- 23. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
880
- 24. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
881
- 25. **\`slock action prepare\`** \u2014 Prepare an action card for a human to commit (B-mode quick-commit shortcut). Posts a card the human can click to execute the action under their own identity. Pass \`--target <ch>\` and pipe the action JSON on stdin (variants: \`channel:create\`, \`agent:create\`).
859
+ 5. **\`slock channel join\`** \u2014 Join a visible public channel. This only affects your own agent membership.
860
+ 6. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
861
+ 7. **\`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.
862
+ 8. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
863
+ 9. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
864
+ 10. **\`slock message react\`** \u2014 Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like \u{1F440}, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
865
+ 11. **\`slock task list\`** \u2014 View a channel's task board.
866
+ 12. **\`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).
867
+ 13. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
868
+ 14. **\`slock task unclaim\`** \u2014 Release your claim on a task.
869
+ 15. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
870
+ 16. **\`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\`.
871
+ 17. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
872
+ 18. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
873
+ 19. **\`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.
874
+ 20. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
875
+ 21. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
876
+ 22. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
877
+ 23. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
878
+ 24. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
879
+ 25. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
880
+ 26. **\`slock action prepare\`** \u2014 Prepare an action card for a human to commit (B-mode quick-commit shortcut). Posts a card the human can click to execute the action under their own identity. Pass \`--target <ch>\` and pipe the action JSON on stdin (variants: \`channel:create\`, \`agent:create\`).
882
881
 
883
882
  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:
884
883
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -893,19 +892,20 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
893
892
  1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
894
893
  2. **${sendCmd}** \u2014 Send a message to a channel or DM.
895
894
  3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
896
- 4. **\`${t("leave_channel")}\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
897
- 5. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
898
- 6. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
899
- 7. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
900
- 8. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
901
- 9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
902
- 10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
903
- 11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
904
- 12. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
905
- 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
906
- 14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
907
- 15. **${listRemindersCmd}** \u2014 List your reminders.
908
- 16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
895
+ 4. **\`${t("join_channel")}\`** \u2014 Join a visible public channel. This only affects your own agent membership.
896
+ 5. **\`${t("leave_channel")}\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
897
+ 6. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
898
+ 7. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
899
+ 8. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
900
+ 9. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
901
+ 10. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
902
+ 11. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
903
+ 12. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
904
+ 13. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
905
+ 14. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
906
+ 15. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
907
+ 16. **${listRemindersCmd}** \u2014 List your reminders.
908
+ 17. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
909
909
  const reminderSection = `### Reminders
910
910
 
911
911
  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, snoozable, updatable, 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.
@@ -926,6 +926,10 @@ Long message with "quotes", $vars, \`backticks\`, and code blocks.
926
926
  EOF
927
927
  \`\`\`
928
928
 
929
+ If Slock says a message was not sent and was saved as a draft, choose one path:
930
+ - To update the draft, use a normal \`slock message send --target <target>\` with the revised content.
931
+ - To send the current draft unchanged, use \`slock message send --send-draft --target <target>\` with no stdin. Do not use \`--send-draft\` when changing content.
932
+
929
933
  **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.` : `### Sending messages
930
934
 
931
935
  - **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
@@ -957,11 +961,11 @@ Threads are sub-conversations attached to a specific message. They let you discu
957
961
  const discoverySection = isCli ? `### Discovering people and channels
958
962
 
959
963
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
960
- 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"\`.
964
+ 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 you join with \`slock channel join --target "#channel-name"\`. Private channels require a human with access to add you. 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"\`.
961
965
  Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
962
966
 
963
967
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
964
- 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")}\`.
968
+ 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 you join with \`${t("join_channel")}\`. Private channels require a human with access to add you. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.
965
969
  Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
966
970
  const channelAwarenessSection = isCli ? `### Channel awareness
967
971
 
@@ -1305,8 +1309,44 @@ ${config.description}. This may evolve.`;
1305
1309
  function buildCliSystemPrompt(config, opts) {
1306
1310
  return buildPrompt(config, "cli", opts);
1307
1311
  }
1308
- function buildMcpSystemPrompt(config, opts) {
1309
- return buildPrompt(config, "mcp", opts);
1312
+
1313
+ // src/slockHome.ts
1314
+ import os from "os";
1315
+ import path from "path";
1316
+ import { existsSync } from "fs";
1317
+ var SLOCK_HOME_ENV = "SLOCK_HOME";
1318
+ function resolveDefaultSlockHome(homeDir = os.homedir()) {
1319
+ return path.resolve(path.join(homeDir, ".slock"));
1320
+ }
1321
+ function resolveSlockHome(env = process.env, homeDir = os.homedir()) {
1322
+ const raw = env[SLOCK_HOME_ENV]?.trim();
1323
+ const root = raw && raw.length > 0 ? raw : resolveDefaultSlockHome(homeDir);
1324
+ return path.resolve(root);
1325
+ }
1326
+ function resolveSlockHomePath(childPath, slockHome = resolveSlockHome()) {
1327
+ return path.join(slockHome, childPath);
1328
+ }
1329
+ function listLegacySlockStatePaths(slockHome = resolveSlockHome(), homeDir = os.homedir()) {
1330
+ const defaultHome = resolveDefaultSlockHome(homeDir);
1331
+ if (path.resolve(slockHome) === defaultHome) return [];
1332
+ const candidates = [
1333
+ {
1334
+ path: path.join(defaultHome, "agents"),
1335
+ destination: path.join(slockHome, "agents"),
1336
+ description: "agent workspaces and per-agent runtime wrapper state"
1337
+ },
1338
+ {
1339
+ path: path.join(defaultHome, "machines"),
1340
+ destination: path.join(slockHome, "machines"),
1341
+ description: "daemon machine locks, local traces, and machine-scoped state"
1342
+ },
1343
+ {
1344
+ path: path.join(defaultHome, "attachments"),
1345
+ destination: path.join(slockHome, "attachments"),
1346
+ description: "chat bridge attachment download cache"
1347
+ }
1348
+ ];
1349
+ return candidates.filter((candidate) => existsSync(candidate.path));
1310
1350
  }
1311
1351
 
1312
1352
  // src/drivers/cliTransport.ts
@@ -1332,23 +1372,23 @@ function prepareCliTransport(ctx, extraEnv = {}, platform = process.platform) {
1332
1372
  if (!ctx.slockCliPath) {
1333
1373
  throw new Error(`${ctx.config.runtime} driver: slockCliPath is required (daemon must inject it)`);
1334
1374
  }
1335
- const slockDir = path.join(ctx.workingDirectory, ".slock");
1375
+ const slockDir = path2.join(ctx.workingDirectory, ".slock");
1336
1376
  mkdirSync(slockDir, { recursive: true });
1337
- const tokenFile = path.join(slockDir, "agent-token");
1377
+ const tokenFile = path2.join(slockDir, "agent-token");
1338
1378
  writeFileSync(tokenFile, ctx.config.authToken || ctx.daemonApiKey, { mode: 384 });
1339
- const posixWrapper = path.join(slockDir, "slock");
1379
+ const posixWrapper = path2.join(slockDir, "slock");
1340
1380
  const posixBody = `#!/usr/bin/env bash
1341
1381
  exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)} "$@"
1342
1382
  `;
1343
1383
  writeFileSync(posixWrapper, posixBody, { mode: 493 });
1344
1384
  if (platform === "win32") {
1345
- const cmdWrapper = path.join(slockDir, "slock.cmd");
1385
+ const cmdWrapper = path2.join(slockDir, "slock.cmd");
1346
1386
  const cmdBody = `@echo off\r
1347
1387
  "${process.execPath}" "${ctx.slockCliPath}" %*\r
1348
1388
  `;
1349
1389
  writeFileSync(cmdWrapper, cmdBody);
1350
1390
  }
1351
- const wrapperPath = platform === "win32" ? path.join(slockDir, "slock.cmd") : posixWrapper;
1391
+ const wrapperPath = platform === "win32" ? path2.join(slockDir, "slock.cmd") : posixWrapper;
1352
1392
  const spawnEnv = {
1353
1393
  ...process.env,
1354
1394
  FORCE_COLOR: "0",
@@ -1360,7 +1400,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1360
1400
  ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
1361
1401
  SLOCK_SERVER_URL: ctx.config.serverUrl,
1362
1402
  SLOCK_AGENT_TOKEN_FILE: tokenFile,
1363
- PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
1403
+ PATH: `${slockDir}${path2.delimiter}${process.env.PATH ?? ""}`
1364
1404
  };
1365
1405
  delete spawnEnv.SLOCK_AGENT_TOKEN;
1366
1406
  return {
@@ -1373,8 +1413,8 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1373
1413
 
1374
1414
  // src/drivers/probe.ts
1375
1415
  import { execFileSync } from "child_process";
1376
- import { existsSync } from "fs";
1377
- import path2 from "path";
1416
+ import { existsSync as existsSync2 } from "fs";
1417
+ import path3 from "path";
1378
1418
  function normalizeExecOutput(raw) {
1379
1419
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
1380
1420
  }
@@ -1417,7 +1457,7 @@ function resolveCommandOnPath(command, deps = {}) {
1417
1457
  }
1418
1458
  }
1419
1459
  function firstExistingPath(candidates, deps = {}) {
1420
- const exists = deps.existsSyncFn ?? existsSync;
1460
+ const exists = deps.existsSyncFn ?? existsSync2;
1421
1461
  for (const candidate of candidates) {
1422
1462
  if (exists(candidate)) return candidate;
1423
1463
  }
@@ -1439,11 +1479,11 @@ function readCommandVersion(command, args = [], deps = {}) {
1439
1479
  }
1440
1480
  function resolveHomePath(relativePath, deps = {}) {
1441
1481
  const homeDir = deps.homeDir ?? deps.env?.HOME ?? process.env.HOME ?? "";
1442
- return path2.join(homeDir, relativePath);
1482
+ return path3.join(homeDir, relativePath);
1443
1483
  }
1444
1484
 
1445
1485
  // src/drivers/claude.ts
1446
- var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
1486
+ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path4.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
1447
1487
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1448
1488
  var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1449
1489
  var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
@@ -1499,27 +1539,27 @@ function readClaudeMcpServers(configPath, vars = {}) {
1499
1539
  }
1500
1540
  function resolveClaudeConfigDir(ctx, home) {
1501
1541
  const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
1502
- return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
1542
+ return configured && path4.isAbsolute(configured) ? configured : path4.join(home, ".claude");
1503
1543
  }
1504
1544
  function collectClaudeMcpConfigFiles(ctx, home) {
1505
1545
  const files = [];
1506
1546
  const pushIfFile = (candidate) => {
1507
1547
  try {
1508
- if (existsSync2(candidate) && statSync(candidate).isFile()) {
1548
+ if (existsSync3(candidate) && statSync(candidate).isFile()) {
1509
1549
  files.push({ path: candidate });
1510
1550
  }
1511
1551
  } catch {
1512
1552
  }
1513
1553
  };
1514
- pushIfFile(path3.join(home, ".claude.json"));
1515
- pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
1516
- const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
1554
+ pushIfFile(path4.join(home, ".claude.json"));
1555
+ pushIfFile(path4.join(ctx.workingDirectory, ".mcp.json"));
1556
+ const pluginRoot = path4.join(resolveClaudeConfigDir(ctx, home), "plugins");
1517
1557
  try {
1518
1558
  for (const entry of readdirSync(pluginRoot)) {
1519
- const pluginPath = path3.join(pluginRoot, entry);
1520
- const configPath = path3.join(pluginPath, ".mcp.json");
1559
+ const pluginPath = path4.join(pluginRoot, entry);
1560
+ const configPath = path4.join(pluginPath, ".mcp.json");
1521
1561
  try {
1522
- if (existsSync2(configPath) && statSync(configPath).isFile()) {
1562
+ if (existsSync3(configPath) && statSync(configPath).isFile()) {
1523
1563
  files.push({
1524
1564
  path: configPath,
1525
1565
  vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
@@ -1623,7 +1663,7 @@ var ClaudeDriver = class {
1623
1663
  ]
1624
1664
  };
1625
1665
  }
1626
- buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
1666
+ buildRuntimeActionsMcpConfig(ctx, home = os2.homedir()) {
1627
1667
  const userMcpServers = buildClaudeUserMcpServers(ctx, home);
1628
1668
  if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
1629
1669
  logger.warn(
@@ -1637,9 +1677,9 @@ var ClaudeDriver = class {
1637
1677
  }
1638
1678
  });
1639
1679
  }
1640
- writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
1641
- const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1642
- const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1680
+ writeClaudeLaunchFiles(ctx, slockDir, home = os2.homedir()) {
1681
+ const systemPromptPath = path4.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1682
+ const mcpConfigPath = path4.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1643
1683
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1644
1684
  writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
1645
1685
  return { systemPromptPath, mcpConfigPath };
@@ -1788,10 +1828,10 @@ var ClaudeDriver = class {
1788
1828
  };
1789
1829
 
1790
1830
  // src/drivers/codex.ts
1791
- import { spawn as spawn2, execSync } from "child_process";
1792
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1793
- import os2 from "os";
1794
- import path4 from "path";
1831
+ import { spawn as spawn2, execFileSync as execFileSync2, execSync } from "child_process";
1832
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1833
+ import os3 from "os";
1834
+ import path5 from "path";
1795
1835
  function getCodexNotificationErrorMessage(params) {
1796
1836
  const topLevelMessage = params?.message;
1797
1837
  if (typeof topLevelMessage === "string" && topLevelMessage.trim()) {
@@ -1804,9 +1844,9 @@ function getCodexNotificationErrorMessage(params) {
1804
1844
  return null;
1805
1845
  }
1806
1846
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1807
- const existsSyncFn = deps.existsSyncFn ?? existsSync3;
1847
+ const existsSyncFn = deps.existsSyncFn ?? existsSync4;
1808
1848
  const execSyncFn = deps.execSyncFn ?? execSync;
1809
- const gitDir = path4.join(workingDirectory, ".git");
1849
+ const gitDir = path5.join(workingDirectory, ".git");
1810
1850
  if (existsSyncFn(gitDir)) return;
1811
1851
  execSyncFn("git init", { cwd: workingDirectory, stdio: "pipe" });
1812
1852
  execSyncFn(
@@ -1818,28 +1858,44 @@ function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1818
1858
  );
1819
1859
  }
1820
1860
  var CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
1821
- function resolveWindowsSandboxRunner(deps = {}) {
1822
- const homeDir = deps.homeDir ?? os2.homedir();
1823
- const sandboxBinDir = path4.join(homeDir, ".codex", ".sandbox-bin");
1824
- if (!(deps.existsSyncFn ?? existsSync3)(sandboxBinDir)) return null;
1861
+ function isWindowsSandboxRunner(commandPath) {
1862
+ return path5.basename(commandPath).toLowerCase().startsWith("codex-command-runner");
1863
+ }
1864
+ function resolveWindowsNpmCodexEntry(deps = {}) {
1865
+ const existsSyncFn = deps.existsSyncFn ?? existsSync4;
1866
+ const execFileSyncFn = deps.execFileSyncFn ?? execFileSync2;
1867
+ const env = deps.env ?? process.env;
1868
+ const winPath = path5.win32;
1825
1869
  try {
1826
- const files = readdirSync2(sandboxBinDir);
1827
- const runners = files.filter((f) => f.startsWith("codex-command-runner") && f.endsWith(".exe")).sort((a, b) => b.localeCompare(a));
1828
- if (runners.length > 0) return path4.join(sandboxBinDir, runners[0]);
1870
+ const globalRoot = String(execFileSyncFn("npm", ["root", "-g"], {
1871
+ encoding: "utf8",
1872
+ stdio: ["ignore", "pipe", "ignore"],
1873
+ env
1874
+ })).trim();
1875
+ const candidate = winPath.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1876
+ if (existsSyncFn(candidate)) return candidate;
1829
1877
  } catch {
1830
1878
  }
1879
+ const cmdPath = resolveCommandOnPath("codex", deps);
1880
+ if (cmdPath) {
1881
+ const candidate = winPath.join(winPath.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1882
+ if (existsSyncFn(candidate)) return candidate;
1883
+ }
1831
1884
  return null;
1832
1885
  }
1833
1886
  function resolveCodexCommand(deps = {}) {
1887
+ const platform = deps.platform ?? process.platform;
1888
+ if (platform === "win32") {
1889
+ const npmEntry = resolveWindowsNpmCodexEntry(deps);
1890
+ if (npmEntry) return npmEntry;
1891
+ const command = resolveCommandOnPath("codex", deps);
1892
+ return command && !isWindowsSandboxRunner(command) ? command : null;
1893
+ }
1834
1894
  const pathCommand = resolveCommandOnPath("codex", deps);
1835
1895
  if (pathCommand) return pathCommand;
1836
- const platform = deps.platform ?? process.platform;
1837
1896
  if (platform === "darwin") {
1838
1897
  return firstExistingPath([CODEX_DESKTOP_BUNDLE_PATH], deps);
1839
1898
  }
1840
- if (platform === "win32") {
1841
- return resolveWindowsSandboxRunner(deps);
1842
- }
1843
1899
  return null;
1844
1900
  }
1845
1901
  function probeCodex(deps = {}) {
@@ -1865,37 +1921,20 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1865
1921
  if ((deps.platform ?? process.platform) !== "win32") {
1866
1922
  return { command: resolveCodexCommand(deps) ?? "codex", args: commandArgs };
1867
1923
  }
1868
- let codexEntry = null;
1869
- try {
1870
- const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1871
- const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1872
- if (existsSync3(candidate)) codexEntry = candidate;
1873
- } catch {
1874
- }
1875
- if (!codexEntry) {
1876
- try {
1877
- const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
1878
- const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1879
- if (existsSync3(candidate)) codexEntry = candidate;
1880
- } catch {
1881
- }
1924
+ const codexEntry = resolveWindowsNpmCodexEntry(deps);
1925
+ if (codexEntry) {
1926
+ return {
1927
+ command: process.execPath,
1928
+ args: [codexEntry, ...commandArgs]
1929
+ };
1882
1930
  }
1883
- if (!codexEntry) {
1884
- const sandboxRunner = resolveWindowsSandboxRunner(deps);
1885
- if (sandboxRunner) {
1886
- return {
1887
- command: sandboxRunner,
1888
- args: commandArgs
1889
- };
1890
- }
1891
- throw new Error(
1892
- "Cannot resolve Codex CLI entry point on Windows. Ensure @openai/codex is installed globally via npm (npm i -g @openai/codex)."
1893
- );
1931
+ const command = resolveCommandOnPath("codex", deps);
1932
+ if (command && !isWindowsSandboxRunner(command)) {
1933
+ return { command, args: commandArgs };
1894
1934
  }
1895
- return {
1896
- command: process.execPath,
1897
- args: [codexEntry, ...commandArgs]
1898
- };
1935
+ throw new Error(
1936
+ "Cannot resolve Codex CLI entry point on Windows. Install Codex Desktop or install @openai/codex globally via npm (npm i -g @openai/codex). Ignoring .codex/.sandbox-bin/codex-command-runner because it is a sandbox helper, not the Codex CLI."
1937
+ );
1899
1938
  }
1900
1939
  function joinReasoningText(item) {
1901
1940
  const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
@@ -2303,9 +2342,9 @@ var CodexDriver = class {
2303
2342
  return detectCodexModels();
2304
2343
  }
2305
2344
  };
2306
- function detectCodexModels(home = os2.homedir()) {
2307
- const cachePath = path4.join(home, ".codex", "models_cache.json");
2308
- const configPath = path4.join(home, ".codex", "config.toml");
2345
+ function detectCodexModels(home = os3.homedir()) {
2346
+ const cachePath = path5.join(home, ".codex", "models_cache.json");
2347
+ const configPath = path5.join(home, ".codex", "config.toml");
2309
2348
  let models = [];
2310
2349
  try {
2311
2350
  const raw = readFileSync2(cachePath, "utf8");
@@ -2335,16 +2374,10 @@ function detectCodexModels(home = os2.homedir()) {
2335
2374
 
2336
2375
  // src/drivers/copilot.ts
2337
2376
  import { spawn as spawn3 } from "child_process";
2338
- import path5 from "path";
2377
+ import path6 from "path";
2339
2378
  import { writeFileSync as writeFileSync3 } from "fs";
2340
2379
  function buildCopilotSpawnEnv(ctx) {
2341
- return {
2342
- ...process.env,
2343
- FORCE_COLOR: "0",
2344
- NO_COLOR: "1",
2345
- ...ctx.config.envVars || {},
2346
- [SLOCK_HOME_ENV]: resolveSlockHome()
2347
- };
2380
+ return prepareCliTransport(ctx, { NO_COLOR: "1" }).spawnEnv;
2348
2381
  }
2349
2382
  var CopilotDriver = class {
2350
2383
  id = "copilot";
@@ -2355,7 +2388,7 @@ var CopilotDriver = class {
2355
2388
  inFlightWake: "spawn_new"
2356
2389
  };
2357
2390
  communication = {
2358
- chat: "mcp_chat_bridge",
2391
+ chat: "slock_cli",
2359
2392
  runtimeControl: "mcp_runtime_actions"
2360
2393
  };
2361
2394
  session = {
@@ -2368,23 +2401,39 @@ var CopilotDriver = class {
2368
2401
  supportsStdinNotification = false;
2369
2402
  mcpToolPrefix = "";
2370
2403
  busyDeliveryMode = "none";
2404
+ usesSlockCliForCommunication = true;
2371
2405
  sessionId = null;
2372
2406
  sessionAnnounced = false;
2373
- spawn(ctx) {
2374
- this.sessionId = ctx.config.sessionId || null;
2375
- this.sessionAnnounced = false;
2407
+ buildRuntimeActionsMcpConfig(ctx) {
2376
2408
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2377
- const mcpCommand = isTsSource ? "npx" : "node";
2378
- const mcpArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
2379
- const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
2380
- writeFileSync3(mcpConfigPath, JSON.stringify({
2409
+ const command = isTsSource ? "npx" : "node";
2410
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2411
+ return JSON.stringify({
2381
2412
  mcpServers: {
2382
2413
  chat: {
2383
- command: mcpCommand,
2384
- args: mcpArgs
2414
+ command,
2415
+ args: [
2416
+ ...bridgeArgs,
2417
+ "--agent-id",
2418
+ ctx.agentId,
2419
+ "--server-url",
2420
+ ctx.config.serverUrl,
2421
+ "--auth-token",
2422
+ ctx.config.authToken || ctx.daemonApiKey,
2423
+ "--runtime",
2424
+ this.id,
2425
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2426
+ "--runtime-actions-only"
2427
+ ]
2385
2428
  }
2386
2429
  }
2387
- }), "utf8");
2430
+ });
2431
+ }
2432
+ spawn(ctx) {
2433
+ this.sessionId = ctx.config.sessionId || null;
2434
+ this.sessionAnnounced = false;
2435
+ const mcpConfigPath = path6.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
2436
+ writeFileSync3(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), "utf8");
2388
2437
  const args = [
2389
2438
  "--output-format",
2390
2439
  "json",
@@ -2485,12 +2534,14 @@ var CopilotDriver = class {
2485
2534
  return null;
2486
2535
  }
2487
2536
  buildSystemPrompt(config, _agentId) {
2488
- return buildMcpSystemPrompt(config, {
2537
+ return buildCliTransportSystemPrompt(config, {
2489
2538
  toolPrefix: "",
2490
2539
  extraCriticalRules: [
2491
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
2540
+ "- 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 `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2541
+ ],
2542
+ postStartupNotes: [
2543
+ "**Copilot 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."
2492
2544
  ],
2493
- postStartupNotes: [],
2494
2545
  includeStdinNotificationSection: false,
2495
2546
  messageNotificationStyle: "poll"
2496
2547
  });
@@ -2499,16 +2550,10 @@ var CopilotDriver = class {
2499
2550
 
2500
2551
  // src/drivers/cursor.ts
2501
2552
  import { spawn as spawn4, spawnSync } from "child_process";
2502
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2503
- import path6 from "path";
2553
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
2554
+ import path7 from "path";
2504
2555
  function buildCursorSpawnEnv(ctx) {
2505
- return {
2506
- ...process.env,
2507
- FORCE_COLOR: "0",
2508
- NO_COLOR: "1",
2509
- ...ctx.config.envVars || {},
2510
- [SLOCK_HOME_ENV]: resolveSlockHome()
2511
- };
2556
+ return prepareCliTransport(ctx, { NO_COLOR: "1" }).spawnEnv;
2512
2557
  }
2513
2558
  var CursorDriver = class {
2514
2559
  id = "cursor";
@@ -2519,7 +2564,7 @@ var CursorDriver = class {
2519
2564
  inFlightWake: "spawn_new"
2520
2565
  };
2521
2566
  communication = {
2522
- chat: "mcp_chat_bridge",
2567
+ chat: "slock_cli",
2523
2568
  runtimeControl: "mcp_runtime_actions"
2524
2569
  };
2525
2570
  session = {
@@ -2532,23 +2577,39 @@ var CursorDriver = class {
2532
2577
  supportsStdinNotification = false;
2533
2578
  mcpToolPrefix = "mcp__chat__";
2534
2579
  busyDeliveryMode = "none";
2535
- spawn(ctx) {
2536
- const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
2537
- if (!existsSync4(cursorDir)) {
2538
- mkdirSync2(cursorDir, { recursive: true });
2539
- }
2580
+ usesSlockCliForCommunication = true;
2581
+ buildRuntimeActionsMcpConfig(ctx) {
2540
2582
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2541
- const mcpCommand = isTsSource ? "npx" : "node";
2542
- const mcpArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
2543
- const mcpConfigPath = path6.join(cursorDir, "mcp.json");
2544
- writeFileSync4(mcpConfigPath, JSON.stringify({
2583
+ const command = isTsSource ? "npx" : "node";
2584
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2585
+ return JSON.stringify({
2545
2586
  mcpServers: {
2546
2587
  chat: {
2547
- command: mcpCommand,
2548
- args: mcpArgs
2588
+ command,
2589
+ args: [
2590
+ ...bridgeArgs,
2591
+ "--agent-id",
2592
+ ctx.agentId,
2593
+ "--server-url",
2594
+ ctx.config.serverUrl,
2595
+ "--auth-token",
2596
+ ctx.config.authToken || ctx.daemonApiKey,
2597
+ "--runtime",
2598
+ this.id,
2599
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2600
+ "--runtime-actions-only"
2601
+ ]
2549
2602
  }
2550
2603
  }
2551
- }), "utf8");
2604
+ });
2605
+ }
2606
+ spawn(ctx) {
2607
+ const cursorDir = path7.join(ctx.workingDirectory, ".cursor");
2608
+ if (!existsSync5(cursorDir)) {
2609
+ mkdirSync2(cursorDir, { recursive: true });
2610
+ }
2611
+ const mcpConfigPath = path7.join(cursorDir, "mcp.json");
2612
+ writeFileSync4(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), "utf8");
2552
2613
  const args = [
2553
2614
  "--print",
2554
2615
  "--output-format",
@@ -2631,12 +2692,14 @@ var CursorDriver = class {
2631
2692
  return null;
2632
2693
  }
2633
2694
  buildSystemPrompt(config, _agentId) {
2634
- return buildMcpSystemPrompt(config, {
2695
+ return buildCliTransportSystemPrompt(config, {
2635
2696
  toolPrefix: "mcp__chat__",
2636
2697
  extraCriticalRules: [
2637
- "- Do NOT use bash/curl/sqlite to send or receive messages. The MCP tools handle everything."
2698
+ "- 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 `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2699
+ ],
2700
+ postStartupNotes: [
2701
+ "**Cursor 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."
2638
2702
  ],
2639
- postStartupNotes: [],
2640
2703
  includeStdinNotificationSection: false,
2641
2704
  messageNotificationStyle: "poll"
2642
2705
  });
@@ -2685,9 +2748,9 @@ function runCursorModelsCommand() {
2685
2748
  }
2686
2749
 
2687
2750
  // src/drivers/gemini.ts
2688
- import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
2689
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
2690
- import path7 from "path";
2751
+ import { execFileSync as execFileSync3, spawn as spawn5 } from "child_process";
2752
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
2753
+ import path8 from "path";
2691
2754
  function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2692
2755
  const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
2693
2756
  if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
@@ -2723,10 +2786,10 @@ function resolveGeminiSpawn(commandArgs, deps = {}) {
2723
2786
  if (platform !== "win32") {
2724
2787
  return { command: resolveCommandOnPath("gemini", deps) ?? "gemini", args: commandArgs };
2725
2788
  }
2726
- const execFileSyncFn = deps.execFileSyncFn ?? execFileSync2;
2727
- const existsSyncFn = deps.existsSyncFn ?? existsSync5;
2789
+ const execFileSyncFn = deps.execFileSyncFn ?? execFileSync3;
2790
+ const existsSyncFn = deps.existsSyncFn ?? existsSync6;
2728
2791
  const env = deps.env ?? process.env;
2729
- const winPath = path7.win32;
2792
+ const winPath = path8.win32;
2730
2793
  let geminiEntry = null;
2731
2794
  try {
2732
2795
  const globalRoot = normalizeExecOutput2(execFileSyncFn("npm", ["root", "-g"], {
@@ -2864,9 +2927,9 @@ var GeminiDriver = class {
2864
2927
  });
2865
2928
  }
2866
2929
  writeGeminiSettings(ctx) {
2867
- const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2930
+ const geminiDir = path8.join(ctx.workingDirectory, ".gemini");
2868
2931
  mkdirSync3(geminiDir, { recursive: true });
2869
- const settingsPath = path7.join(geminiDir, "settings.json");
2932
+ const settingsPath = path8.join(geminiDir, "settings.json");
2870
2933
  writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
2871
2934
  }
2872
2935
  buildRuntimeActionsMcpSettings(ctx) {
@@ -2899,9 +2962,9 @@ var GeminiDriver = class {
2899
2962
  // src/drivers/kimi.ts
2900
2963
  import { randomUUID } from "crypto";
2901
2964
  import { spawn as spawn6 } from "child_process";
2902
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2903
- import os3 from "os";
2904
- import path8 from "path";
2965
+ import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2966
+ import os4 from "os";
2967
+ import path9 from "path";
2905
2968
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2906
2969
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
2907
2970
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
@@ -2922,7 +2985,7 @@ var KimiDriver = class {
2922
2985
  inFlightWake: "steer"
2923
2986
  };
2924
2987
  communication = {
2925
- chat: "mcp_chat_bridge",
2988
+ chat: "slock_cli",
2926
2989
  runtimeControl: "mcp_runtime_actions"
2927
2990
  };
2928
2991
  session = {
@@ -2935,6 +2998,7 @@ var KimiDriver = class {
2935
2998
  supportsStdinNotification = true;
2936
2999
  mcpToolPrefix = "";
2937
3000
  busyDeliveryMode = "direct";
3001
+ usesSlockCliForCommunication = true;
2938
3002
  sessionId = null;
2939
3003
  sessionAnnounced = false;
2940
3004
  promptRequestId = null;
@@ -2950,7 +3014,8 @@ var KimiDriver = class {
2950
3014
  ctx.config.authToken || ctx.daemonApiKey,
2951
3015
  "--runtime",
2952
3016
  "kimi",
2953
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : []
3017
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
3018
+ "--runtime-actions-only"
2954
3019
  ];
2955
3020
  }
2956
3021
  spawn(ctx) {
@@ -2961,10 +3026,10 @@ var KimiDriver = class {
2961
3026
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2962
3027
  const command = isTsSource ? "npx" : "node";
2963
3028
  const bridgeArgs = this.buildChatBridgeArgs(ctx);
2964
- const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2965
- const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2966
- const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
2967
- if (!isResume || !existsSync6(systemPromptPath)) {
3029
+ const systemPromptPath = path9.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
3030
+ const agentFilePath = path9.join(ctx.workingDirectory, KIMI_AGENT_FILE);
3031
+ const mcpConfigPath = path9.join(ctx.workingDirectory, KIMI_MCP_FILE);
3032
+ if (!isResume || !existsSync7(systemPromptPath)) {
2968
3033
  writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
2969
3034
  }
2970
3035
  writeFileSync6(agentFilePath, [
@@ -2995,7 +3060,7 @@ var KimiDriver = class {
2995
3060
  if (ctx.config.model && ctx.config.model !== "default") {
2996
3061
  args.push("--model", ctx.config.model);
2997
3062
  }
2998
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
3063
+ const spawnEnv = prepareCliTransport(ctx, { NO_COLOR: "1" }).spawnEnv;
2999
3064
  const proc = spawn6("kimi", args, {
3000
3065
  cwd: ctx.workingDirectory,
3001
3066
  stdio: ["pipe", "pipe", "pipe"],
@@ -3102,10 +3167,10 @@ var KimiDriver = class {
3102
3167
  });
3103
3168
  }
3104
3169
  buildSystemPrompt(config, _agentId) {
3105
- return buildMcpSystemPrompt(config, {
3170
+ return buildCliTransportSystemPrompt(config, {
3106
3171
  toolPrefix: "",
3107
3172
  extraCriticalRules: [
3108
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
3173
+ "- 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 `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
3109
3174
  ],
3110
3175
  postStartupNotes: [],
3111
3176
  includeStdinNotificationSection: true,
@@ -3116,8 +3181,8 @@ var KimiDriver = class {
3116
3181
  return detectKimiModels();
3117
3182
  }
3118
3183
  };
3119
- function detectKimiModels(home = os3.homedir()) {
3120
- const configPath = path8.join(home, ".kimi", "config.toml");
3184
+ function detectKimiModels(home = os4.homedir()) {
3185
+ const configPath = path9.join(home, ".kimi", "config.toml");
3121
3186
  let raw;
3122
3187
  try {
3123
3188
  raw = readFileSync3(configPath, "utf8");
@@ -3145,8 +3210,8 @@ function detectKimiModels(home = os3.homedir()) {
3145
3210
  // src/drivers/opencode.ts
3146
3211
  import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
3147
3212
  import { readFileSync as readFileSync4 } from "fs";
3148
- import os4 from "os";
3149
- import path9 from "path";
3213
+ import os5 from "os";
3214
+ import path10 from "path";
3150
3215
  var CHAT_MCP_SERVER_NAME = "chat";
3151
3216
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
3152
3217
  var SLOCK_AGENT_NAME = "slock";
@@ -3193,8 +3258,8 @@ function parseUserOpenCodeConfig(ctx) {
3193
3258
  const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
3194
3259
  return parseOpenCodeConfigContent(raw);
3195
3260
  }
3196
- function readLocalOpenCodeConfig(home = os4.homedir()) {
3197
- const configPath = path9.join(home, ".config", "opencode", "opencode.json");
3261
+ function readLocalOpenCodeConfig(home = os5.homedir()) {
3262
+ const configPath = path10.join(home, ".config", "opencode", "opencode.json");
3198
3263
  try {
3199
3264
  return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
3200
3265
  } catch {
@@ -3242,7 +3307,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
3242
3307
  }
3243
3308
  };
3244
3309
  }
3245
- function buildOpenCodeConfig(ctx, home = os4.homedir()) {
3310
+ function buildOpenCodeConfig(ctx, home = os5.homedir()) {
3246
3311
  const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
3247
3312
  const userAgents = recordField(userConfig.agent);
3248
3313
  const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
@@ -3267,7 +3332,7 @@ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
3267
3332
  }
3268
3333
  };
3269
3334
  }
3270
- function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
3335
+ function buildOpenCodeLaunchOptions(ctx, home = os5.homedir()) {
3271
3336
  const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
3272
3337
  const config = buildOpenCodeConfig(ctx, home);
3273
3338
  const env = {
@@ -3363,7 +3428,7 @@ function formatOpenCodeLabelToken(token) {
3363
3428
  if (/^\d/.test(token)) return token;
3364
3429
  return normalized.charAt(0).toUpperCase() + normalized.slice(1);
3365
3430
  }
3366
- function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3431
+ function detectOpenCodeModels(home = os5.homedir(), runCommand = runOpenCodeModelsCommand) {
3367
3432
  const commandResult = runCommand(home);
3368
3433
  if (commandResult.error || commandResult.status !== 0) return null;
3369
3434
  return parseOpenCodeModelsOutput(commandResult.stdout);
@@ -3547,7 +3612,7 @@ function getDriver(runtimeId) {
3547
3612
 
3548
3613
  // src/workspaces.ts
3549
3614
  import { readdir, rm, stat } from "fs/promises";
3550
- import path10 from "path";
3615
+ import path11 from "path";
3551
3616
  function isValidWorkspaceDirectoryName(directoryName) {
3552
3617
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
3553
3618
  }
@@ -3555,7 +3620,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
3555
3620
  if (!isValidWorkspaceDirectoryName(directoryName)) {
3556
3621
  return null;
3557
3622
  }
3558
- return path10.join(dataDir, directoryName);
3623
+ return path11.join(dataDir, directoryName);
3559
3624
  }
3560
3625
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
3561
3626
  return {
@@ -3604,7 +3669,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
3604
3669
  return summary;
3605
3670
  }
3606
3671
  const childSummaries = await Promise.all(
3607
- entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
3672
+ entries.map((entry) => summarizeWorkspaceEntry(path11.join(dirPath, entry.name), entry))
3608
3673
  );
3609
3674
  for (const childSummary of childSummaries) {
3610
3675
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -3623,7 +3688,7 @@ async function scanWorkspaceDirectories(dataDir) {
3623
3688
  if (!entry.isDirectory()) {
3624
3689
  return null;
3625
3690
  }
3626
- const dirPath = path10.join(dataDir, entry.name);
3691
+ const dirPath = path11.join(dataDir, entry.name);
3627
3692
  try {
3628
3693
  const summary = await summarizeWorkspaceDirectory(dirPath);
3629
3694
  return {
@@ -3780,6 +3845,12 @@ function readNonNegativeIntegerEnv(name, fallback) {
3780
3845
  if (!Number.isFinite(parsed) || parsed < 0) return fallback;
3781
3846
  return Math.floor(parsed);
3782
3847
  }
3848
+ function stalledRecoverySigtermTimeoutMs() {
3849
+ return readNonNegativeIntegerEnv(
3850
+ "SLOCK_DAEMON_STALLED_RECOVERY_SIGTERM_TIMEOUT_MS",
3851
+ DEFAULT_STALLED_RECOVERY_SIGTERM_TIMEOUT_MS
3852
+ );
3853
+ }
3783
3854
  function toLocalTime(iso) {
3784
3855
  const d = new Date(iso);
3785
3856
  if (isNaN(d.getTime())) return iso;
@@ -3814,19 +3885,19 @@ function findSessionJsonl(root, predicate) {
3814
3885
  if (depth < 0 || visited >= maxEntries) return null;
3815
3886
  let entries;
3816
3887
  try {
3817
- entries = readdirSync3(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3888
+ entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3818
3889
  } catch {
3819
3890
  return null;
3820
3891
  }
3821
3892
  for (const entry of entries) {
3822
3893
  if (++visited > maxEntries) return null;
3823
3894
  if (!entry.isFile() || !predicate(entry.name)) continue;
3824
- return path11.join(dir, entry.name);
3895
+ return path12.join(dir, entry.name);
3825
3896
  }
3826
3897
  for (const entry of entries) {
3827
3898
  if (++visited > maxEntries) return null;
3828
3899
  if (!entry.isDirectory()) continue;
3829
- const found = visit(path11.join(dir, entry.name), depth - 1);
3900
+ const found = visit(path12.join(dir, entry.name), depth - 1);
3830
3901
  if (found) return found;
3831
3902
  }
3832
3903
  return null;
@@ -3839,9 +3910,9 @@ function safeSessionFilename(value) {
3839
3910
  }
3840
3911
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3841
3912
  try {
3842
- const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
3913
+ const dir = path12.join(fallbackDir, ".slock", "runtime-sessions");
3843
3914
  mkdirSync4(dir, { recursive: true });
3844
- const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3915
+ const filePath = path12.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3845
3916
  writeFileSync7(filePath, JSON.stringify({
3846
3917
  type: "runtime_session_handoff",
3847
3918
  runtime,
@@ -3860,8 +3931,8 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3860
3931
  return null;
3861
3932
  }
3862
3933
  }
3863
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3864
- const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3934
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os6.homedir(), fallbackDir) {
3935
+ const directPath = path12.isAbsolute(sessionId) ? sessionId : null;
3865
3936
  if (directPath) {
3866
3937
  try {
3867
3938
  if (statSync2(directPath).isFile()) {
@@ -3870,7 +3941,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), f
3870
3941
  } catch {
3871
3942
  }
3872
3943
  }
3873
- 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;
3944
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path12.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path12.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
3874
3945
  if (!resolvedPath && fallbackDir) {
3875
3946
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
3876
3947
  if (fallback) return fallback;
@@ -4011,6 +4082,7 @@ var TRAJECTORY_COALESCE_MS = 350;
4011
4082
  var ACTIVITY_HEARTBEAT_MS = 6e4;
4012
4083
  var COMPACTION_STALE_MS = 5 * 6e4;
4013
4084
  var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
4085
+ var DEFAULT_STALLED_RECOVERY_SIGTERM_TIMEOUT_MS = 1e4;
4014
4086
  var MAX_STDOUT_LINES = 8;
4015
4087
  var MAX_STDOUT_LINE_LENGTH = 240;
4016
4088
  var MAX_STDERR_LINES = 8;
@@ -4410,6 +4482,17 @@ Success = user starts useful collaboration and setup progresses,
4410
4482
  not finishing a long onboarding conversation in one channel.
4411
4483
  `;
4412
4484
  }
4485
+ function createRuntimeTraceCounters() {
4486
+ return {
4487
+ events: 0,
4488
+ toolCalls: 0,
4489
+ toolOutputs: 0,
4490
+ compactionStarts: 0,
4491
+ compactionFinishes: 0,
4492
+ textEvents: 0,
4493
+ thinkingEvents: 0
4494
+ };
4495
+ }
4413
4496
  function createGatedSteeringState() {
4414
4497
  return {
4415
4498
  phase: "idle",
@@ -4588,8 +4671,111 @@ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
4588
4671
  outstandingToolUses: ap.gatedSteering.outstandingToolUses,
4589
4672
  compacting: ap.gatedSteering.compacting,
4590
4673
  recentStderrCount: ap.recentStderr.length,
4591
- recentStdoutCount: ap.recentStdout.length
4674
+ recentStdoutCount: ap.recentStdout.length,
4675
+ ...runtimeTraceCounterAttrs(ap)
4676
+ }
4677
+ };
4678
+ }
4679
+ function bucketBytes(value) {
4680
+ const bytes = typeof value === "string" ? Buffer.byteLength(value, "utf8") : Math.max(0, Math.floor(value ?? 0));
4681
+ if (bytes === 0) return "0";
4682
+ if (bytes < 1024) return "<1KB";
4683
+ if (bytes < 10 * 1024) return "1KB-10KB";
4684
+ if (bytes < 100 * 1024) return "10KB-100KB";
4685
+ if (bytes < 1024 * 1024) return "100KB-1MB";
4686
+ return "1MB+";
4687
+ }
4688
+ function attachmentBytesBucket(bytes, knownCount) {
4689
+ return knownCount > 0 ? bucketBytes(bytes) : "unknown";
4690
+ }
4691
+ function summarizeMessageInputBytes(messages) {
4692
+ if (!messages || messages.length === 0) {
4693
+ return {
4694
+ runtime_input_messages_count: 0,
4695
+ runtime_input_messages_content_bytes_bucket: "0",
4696
+ runtime_input_attachments_count: 0,
4697
+ runtime_input_image_attachments_count: 0,
4698
+ runtime_input_attachments_size_known_count: 0,
4699
+ runtime_input_attachments_bytes_bucket: "0",
4700
+ runtime_input_image_attachments_size_known_count: 0,
4701
+ runtime_input_image_attachments_bytes_bucket: "0",
4702
+ runtime_input_largest_attachment_bytes_bucket: "0",
4703
+ runtime_input_thread_context_messages_count: 0,
4704
+ runtime_input_thread_context_content_bytes_bucket: "0"
4705
+ };
4706
+ }
4707
+ let contentBytes = 0;
4708
+ let attachmentCount = 0;
4709
+ let imageAttachmentCount = 0;
4710
+ let attachmentSizeKnownCount = 0;
4711
+ let attachmentBytes = 0;
4712
+ let imageAttachmentSizeKnownCount = 0;
4713
+ let imageAttachmentBytes = 0;
4714
+ let largestAttachmentBytes = 0;
4715
+ let threadContextMessages = 0;
4716
+ let threadContextContentBytes = 0;
4717
+ for (const message of messages) {
4718
+ contentBytes += Buffer.byteLength(message.content || "", "utf8");
4719
+ for (const attachment of message.attachments || []) {
4720
+ attachmentCount++;
4721
+ if (typeof attachment.sizeBytes === "number" && Number.isFinite(attachment.sizeBytes) && attachment.sizeBytes >= 0) {
4722
+ attachmentSizeKnownCount++;
4723
+ attachmentBytes += attachment.sizeBytes;
4724
+ largestAttachmentBytes = Math.max(largestAttachmentBytes, attachment.sizeBytes);
4725
+ }
4726
+ if (attachment.mimeType?.startsWith("image/")) {
4727
+ imageAttachmentCount++;
4728
+ if (typeof attachment.sizeBytes === "number" && Number.isFinite(attachment.sizeBytes) && attachment.sizeBytes >= 0) {
4729
+ imageAttachmentSizeKnownCount++;
4730
+ imageAttachmentBytes += attachment.sizeBytes;
4731
+ }
4732
+ }
4733
+ }
4734
+ const joinContext = message.thread_join_context;
4735
+ if (joinContext) {
4736
+ const contextMessages = [joinContext.parent_message, ...joinContext.recent_messages];
4737
+ threadContextMessages += contextMessages.length;
4738
+ for (const contextMessage of contextMessages) {
4739
+ threadContextContentBytes += Buffer.byteLength(contextMessage.content || "", "utf8");
4740
+ }
4592
4741
  }
4742
+ }
4743
+ return {
4744
+ runtime_input_messages_count: messages.length,
4745
+ runtime_input_messages_content_bytes_bucket: bucketBytes(contentBytes),
4746
+ runtime_input_attachments_count: attachmentCount,
4747
+ runtime_input_image_attachments_count: imageAttachmentCount,
4748
+ runtime_input_attachments_size_known_count: attachmentSizeKnownCount,
4749
+ runtime_input_attachments_bytes_bucket: attachmentCount > 0 ? attachmentBytesBucket(attachmentBytes, attachmentSizeKnownCount) : "0",
4750
+ runtime_input_image_attachments_size_known_count: imageAttachmentSizeKnownCount,
4751
+ runtime_input_image_attachments_bytes_bucket: imageAttachmentCount > 0 ? attachmentBytesBucket(imageAttachmentBytes, imageAttachmentSizeKnownCount) : "0",
4752
+ runtime_input_largest_attachment_bytes_bucket: attachmentCount > 0 ? attachmentBytesBucket(largestAttachmentBytes, attachmentSizeKnownCount) : "0",
4753
+ runtime_input_thread_context_messages_count: threadContextMessages,
4754
+ runtime_input_thread_context_content_bytes_bucket: bucketBytes(threadContextContentBytes)
4755
+ };
4756
+ }
4757
+ function buildRuntimeInputTraceAttrs(opts) {
4758
+ return {
4759
+ runtime_input_source: opts.source,
4760
+ runtime_input_prompt_bytes_bucket: bucketBytes(opts.prompt),
4761
+ runtime_input_standing_prompt_bytes_bucket: bucketBytes(opts.standingPrompt),
4762
+ runtime_input_resume_prompt_present: Boolean(opts.resumePrompt),
4763
+ runtime_input_resume_prompt_bytes_bucket: bucketBytes(opts.resumePrompt),
4764
+ runtime_input_session_present: opts.sessionIdPresent,
4765
+ runtime_input_native_standing_prompt_present: opts.nativeStandingPrompt,
4766
+ runtime_input_unread_channels_count: opts.unreadSummary ? Object.keys(opts.unreadSummary).length : 0,
4767
+ ...summarizeMessageInputBytes(opts.messages)
4768
+ };
4769
+ }
4770
+ function runtimeTraceCounterAttrs(ap) {
4771
+ return {
4772
+ runtime_events_count: ap.runtimeTraceCounters.events,
4773
+ runtime_tool_calls_count: ap.runtimeTraceCounters.toolCalls,
4774
+ runtime_tool_outputs_count: ap.runtimeTraceCounters.toolOutputs,
4775
+ runtime_compaction_starts_count: ap.runtimeTraceCounters.compactionStarts,
4776
+ runtime_compaction_finishes_count: ap.runtimeTraceCounters.compactionFinishes,
4777
+ runtime_text_events_count: ap.runtimeTraceCounters.textEvents,
4778
+ runtime_thinking_events_count: ap.runtimeTraceCounters.thinkingEvents
4593
4779
  };
4594
4780
  }
4595
4781
  function getMessageDeliveryText(driver) {
@@ -4645,7 +4831,7 @@ var AgentProcessManager = class _AgentProcessManager {
4645
4831
  this.daemonApiKey = daemonApiKey;
4646
4832
  this.serverUrl = opts.serverUrl;
4647
4833
  this.dataDir = opts.dataDir || resolveSlockHomePath("agents");
4648
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
4834
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os6.homedir();
4649
4835
  this.driverResolver = opts.driverResolver || getDriver;
4650
4836
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
4651
4837
  this.tracer = opts.tracer ?? noopTracer;
@@ -4889,26 +5075,26 @@ var AgentProcessManager = class _AgentProcessManager {
4889
5075
  this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4890
5076
  try {
4891
5077
  const driver = this.driverResolver(config.runtime || "claude");
4892
- const agentDataDir = path11.join(this.dataDir, agentId);
5078
+ const agentDataDir = path12.join(this.dataDir, agentId);
4893
5079
  await mkdir(agentDataDir, { recursive: true });
4894
5080
  const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
4895
- const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
5081
+ const memoryMdPath = path12.join(agentDataDir, "MEMORY.md");
4896
5082
  try {
4897
5083
  await access(memoryMdPath);
4898
5084
  } catch {
4899
5085
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
4900
5086
  await writeFile(memoryMdPath, initialMemoryMd);
4901
5087
  }
4902
- const notesDir = path11.join(agentDataDir, "notes");
5088
+ const notesDir = path12.join(agentDataDir, "notes");
4903
5089
  await mkdir(notesDir, { recursive: true });
4904
5090
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
4905
5091
  const seedFiles = buildOnboardingSeedFiles();
4906
5092
  for (const { relativePath, content } of seedFiles) {
4907
- const fullPath = path11.join(agentDataDir, relativePath);
5093
+ const fullPath = path12.join(agentDataDir, relativePath);
4908
5094
  try {
4909
5095
  await access(fullPath);
4910
5096
  } catch {
4911
- await mkdir(path11.dirname(fullPath), { recursive: true });
5097
+ await mkdir(path12.dirname(fullPath), { recursive: true });
4912
5098
  await writeFile(fullPath, content);
4913
5099
  }
4914
5100
  }
@@ -4916,17 +5102,21 @@ var AgentProcessManager = class _AgentProcessManager {
4916
5102
  const isResume = !!runtimeConfig.sessionId;
4917
5103
  const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
4918
5104
  let prompt;
5105
+ let promptSource;
4919
5106
  if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
4920
5107
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
5108
+ promptSource = "runtime_profile_control";
4921
5109
  } else if (isResume && resumePrompt) {
4922
5110
  prompt = resumePrompt;
4923
5111
  prompt += getBusyDeliveryNote(driver);
5112
+ promptSource = "resume_prompt";
4924
5113
  } else if (wakeMessage) {
4925
5114
  const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
4926
5115
  const channelLabel = formatChannelLabel(wakeMessage);
4927
5116
  prompt = runtimeProfileControlPrompt ?? `New message received:
4928
5117
 
4929
5118
  ${formatIncomingMessage(wakeMessage, driver)}`;
5119
+ promptSource = runtimeProfileControlPrompt ? "runtime_profile_control_message" : "wake_message";
4930
5120
  if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
4931
5121
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
4932
5122
  if (otherUnread.length > 0) {
@@ -4960,12 +5150,25 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
4960
5150
  prompt += `
4961
5151
 
4962
5152
  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)}`;
5153
+ promptSource = "resume_unread_summary";
4963
5154
  } else if (isResume) {
4964
5155
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
4965
5156
  prompt += getBusyDeliveryNote(driver);
5157
+ promptSource = "resume_empty";
4966
5158
  } else {
4967
5159
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
5160
+ promptSource = "cold_start";
4968
5161
  }
5162
+ const runtimeInputTraceAttrs = buildRuntimeInputTraceAttrs({
5163
+ source: promptSource,
5164
+ prompt,
5165
+ standingPrompt,
5166
+ resumePrompt,
5167
+ messages: wakeMessage ? [wakeMessage] : void 0,
5168
+ unreadSummary,
5169
+ sessionIdPresent: isResume,
5170
+ nativeStandingPrompt: Boolean(driver.supportsNativeStandingPrompt)
5171
+ });
4969
5172
  const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
4970
5173
  const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
4971
5174
  if (canDeferEmptyStart) {
@@ -5026,6 +5229,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5026
5229
  lastRuntimeEventAt: Date.now(),
5027
5230
  runtimeProgressStaleSince: null,
5028
5231
  runtimeTraceSpan: null,
5232
+ runtimeTraceCounters: createRuntimeTraceCounters(),
5029
5233
  lastActivity: "",
5030
5234
  lastActivityDetail: "",
5031
5235
  activityClientSeq: 0,
@@ -5036,6 +5240,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5036
5240
  exitCode: null,
5037
5241
  exitSignal: null,
5038
5242
  expectedTerminationReason: null,
5243
+ stalledRecoverySigtermTimer: null,
5039
5244
  runtimeProfileTurnControl: runtimeConfig.runtimeProfileControl ? runtimeProfileTurnControl(runtimeConfig.runtimeProfileControl.kind, runtimeConfig.runtimeProfileControl.key, "agent_config") : null,
5040
5245
  pendingTrajectory: null,
5041
5246
  gatedSteering: createGatedSteeringState()
@@ -5047,7 +5252,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5047
5252
  sessionId: effectiveConfig.sessionId || null,
5048
5253
  launchId: launchId || null
5049
5254
  });
5050
- this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
5255
+ this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0, runtimeInputTraceAttrs);
5051
5256
  this.agentsStarting.delete(agentId);
5052
5257
  if (config.runtimeProfileControl) {
5053
5258
  this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
@@ -5129,6 +5334,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5129
5334
  if (ap.activityHeartbeat) {
5130
5335
  clearInterval(ap.activityHeartbeat);
5131
5336
  }
5337
+ this.clearStalledRecoverySigtermWatchdog(ap);
5132
5338
  const finalCode = ap.exitCode ?? code;
5133
5339
  const finalSignal = ap.exitSignal ?? signal;
5134
5340
  const expectedTermination = Boolean(ap.expectedTerminationReason);
@@ -5141,6 +5347,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5141
5347
  expectedTerminationReason: ap.expectedTerminationReason || void 0,
5142
5348
  exitCode: finalCode,
5143
5349
  exitSignal: finalSignal,
5350
+ ...runtimeTraceCounterAttrs(ap),
5144
5351
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "process_exit")
5145
5352
  });
5146
5353
  if (processEndedCleanly) {
@@ -5363,6 +5570,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5363
5570
  clearInterval(ap.activityHeartbeat);
5364
5571
  }
5365
5572
  this.clearCompactionWatchdog(ap);
5573
+ this.clearStalledRecoverySigtermWatchdog(ap);
5366
5574
  this.agents.delete(agentId);
5367
5575
  this.processExitTraceAttrs.set(ap.process, {
5368
5576
  stop_source: silent ? "daemon_internal" : "explicit_request",
@@ -5568,7 +5776,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5568
5776
  return true;
5569
5777
  }
5570
5778
  async resetWorkspace(agentId) {
5571
- const agentDataDir = path11.join(this.dataDir, agentId);
5779
+ const agentDataDir = path12.join(this.dataDir, agentId);
5572
5780
  try {
5573
5781
  await rm2(agentDataDir, { recursive: true, force: true });
5574
5782
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -5605,7 +5813,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5605
5813
  return result;
5606
5814
  }
5607
5815
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
5608
- const workspacePath = path11.join(this.dataDir, agentId);
5816
+ const workspacePath = path12.join(this.dataDir, agentId);
5609
5817
  return {
5610
5818
  agentId,
5611
5819
  launchId,
@@ -5853,7 +6061,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5853
6061
  }
5854
6062
  // Workspace file browsing
5855
6063
  async getFileTree(agentId, dirPath) {
5856
- const agentDir = path11.join(this.dataDir, agentId);
6064
+ const agentDir = path12.join(this.dataDir, agentId);
5857
6065
  try {
5858
6066
  await stat2(agentDir);
5859
6067
  } catch {
@@ -5861,8 +6069,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5861
6069
  }
5862
6070
  let targetDir = agentDir;
5863
6071
  if (dirPath) {
5864
- const resolved = path11.resolve(agentDir, dirPath);
5865
- if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
6072
+ const resolved = path12.resolve(agentDir, dirPath);
6073
+ if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
5866
6074
  return [];
5867
6075
  }
5868
6076
  targetDir = resolved;
@@ -5870,14 +6078,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5870
6078
  return this.listDirectoryChildren(targetDir, agentDir);
5871
6079
  }
5872
6080
  async readFile(agentId, filePath) {
5873
- const agentDir = path11.join(this.dataDir, agentId);
5874
- const resolved = path11.resolve(agentDir, filePath);
5875
- if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
6081
+ const agentDir = path12.join(this.dataDir, agentId);
6082
+ const resolved = path12.resolve(agentDir, filePath);
6083
+ if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
5876
6084
  throw new Error("Access denied");
5877
6085
  }
5878
6086
  const info = await stat2(resolved);
5879
6087
  if (info.isDirectory()) throw new Error("Cannot read a directory");
5880
- const ext = path11.extname(resolved).toLowerCase();
6088
+ const ext = path12.extname(resolved).toLowerCase();
5881
6089
  if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5882
6090
  if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5883
6091
  const content = await readFile(resolved, "utf-8");
@@ -5911,14 +6119,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5911
6119
  async listSkills(agentId, runtimeHint) {
5912
6120
  const agent = this.agents.get(agentId);
5913
6121
  const runtime = runtimeHint || agent?.config.runtime || "claude";
5914
- const home = os5.homedir();
5915
- const workspaceDir = path11.join(this.dataDir, agentId);
6122
+ const home = os6.homedir();
6123
+ const workspaceDir = path12.join(this.dataDir, agentId);
5916
6124
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
5917
6125
  const globalResults = await Promise.all(
5918
- paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
6126
+ paths.global.map((p) => this.scanSkillsDir(path12.join(home, p)))
5919
6127
  );
5920
6128
  const workspaceResults = await Promise.all(
5921
- paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
6129
+ paths.workspace.map((p) => this.scanSkillsDir(path12.join(workspaceDir, p)))
5922
6130
  );
5923
6131
  const dedup = (skills) => {
5924
6132
  const seen = /* @__PURE__ */ new Set();
@@ -5947,7 +6155,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5947
6155
  const skills = [];
5948
6156
  for (const entry of entries) {
5949
6157
  if (entry.isDirectory() || entry.isSymbolicLink()) {
5950
- const skillMd = path11.join(dir, entry.name, "SKILL.md");
6158
+ const skillMd = path12.join(dir, entry.name, "SKILL.md");
5951
6159
  try {
5952
6160
  const content = await readFile(skillMd, "utf-8");
5953
6161
  const skill = this.parseSkillMd(entry.name, content);
@@ -5958,7 +6166,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5958
6166
  } else if (entry.name.endsWith(".md")) {
5959
6167
  const cmdName = entry.name.replace(/\.md$/, "");
5960
6168
  try {
5961
- const content = await readFile(path11.join(dir, entry.name), "utf-8");
6169
+ const content = await readFile(path12.join(dir, entry.name), "utf-8");
5962
6170
  const skill = this.parseSkillMd(cmdName, content);
5963
6171
  skill.sourcePath = dir;
5964
6172
  skills.push(skill);
@@ -5993,6 +6201,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5993
6201
  /**
5994
6202
  * Broadcast an activity change — emits a single agent:activity event that carries
5995
6203
  * both the status (for the dot indicator) and trajectory entries (for the activity log).
6204
+ *
6205
+ * TODO(lifecycle-v2/daemon-protocol): split this legacy frame into
6206
+ * structured lifecycle producers. Runtime progress, transient heartbeat,
6207
+ * provider/runtime error, process exit, and user-visible activity entries
6208
+ * should carry explicit reason/correlation/window attrs so the server no
6209
+ * longer infers lifecycle semantics from generic activity strings.
5996
6210
  */
5997
6211
  broadcastActivity(agentId, activity, detail, extraTrajectory = [], launchIdOverride) {
5998
6212
  const ap = this.agents.get(agentId);
@@ -6121,6 +6335,64 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6121
6335
  }
6122
6336
  ap.compactionStartedAt = null;
6123
6337
  }
6338
+ clearStalledRecoverySigtermWatchdog(ap) {
6339
+ if (!ap.stalledRecoverySigtermTimer) return;
6340
+ clearTimeout(ap.stalledRecoverySigtermTimer);
6341
+ ap.stalledRecoverySigtermTimer = null;
6342
+ }
6343
+ mergeProcessExitTraceAttrs(proc, attrs) {
6344
+ this.processExitTraceAttrs.set(proc, {
6345
+ ...this.processExitTraceAttrs.get(proc) ?? {},
6346
+ ...attrs
6347
+ });
6348
+ }
6349
+ startStalledRecoverySigtermWatchdog(agentId, ap, runtimeLabel, queuedMessagesAtSignal, staleForMs) {
6350
+ this.clearStalledRecoverySigtermWatchdog(ap);
6351
+ const timeoutMs = stalledRecoverySigtermTimeoutMs();
6352
+ const processAtSignal = ap.process;
6353
+ ap.stalledRecoverySigtermTimer = setTimeout(() => {
6354
+ ap.stalledRecoverySigtermTimer = null;
6355
+ const current = this.agents.get(agentId);
6356
+ if (!current || current !== ap || current.process !== processAtSignal || current.expectedTerminationReason !== "stalled_recovery") {
6357
+ return;
6358
+ }
6359
+ this.mergeProcessExitTraceAttrs(processAtSignal, {
6360
+ stalled_recovery_sigterm_timeout: true,
6361
+ stalled_recovery_sigterm_timeout_ms: timeoutMs
6362
+ });
6363
+ this.recordDaemonTrace("daemon.agent.stalled_recovery.sigterm_timeout", {
6364
+ agentId,
6365
+ launchId: current.launchId || void 0,
6366
+ runtime: current.config.runtime,
6367
+ model: current.config.model,
6368
+ runtime_label: runtimeLabel,
6369
+ queued_messages_count: current.inbox.length,
6370
+ queued_messages_at_signal: queuedMessagesAtSignal,
6371
+ stale_age_ms_at_signal: staleForMs,
6372
+ timeout_ms: timeoutMs,
6373
+ process_pid_present: typeof processAtSignal.pid === "number",
6374
+ session_id_present: Boolean(current.sessionId),
6375
+ supports_stdin_notification: current.driver.supportsStdinNotification,
6376
+ busy_delivery_mode: current.driver.busyDeliveryMode
6377
+ }, "error");
6378
+ logger.warn(
6379
+ `[Agent ${agentId}] Stalled ${runtimeLabel} runtime did not exit after SIGTERM within ${timeoutMs}ms; force killing`
6380
+ );
6381
+ try {
6382
+ processAtSignal.kill("SIGKILL");
6383
+ } catch (err) {
6384
+ const reason = err instanceof Error ? err.message : String(err);
6385
+ this.recordDaemonTrace("daemon.agent.stalled_recovery.sigkill_failed", {
6386
+ agentId,
6387
+ launchId: current.launchId || void 0,
6388
+ runtime: current.config.runtime,
6389
+ model: current.config.model,
6390
+ reason
6391
+ }, "error");
6392
+ logger.warn(`[Agent ${agentId}] Failed to force kill stalled ${runtimeLabel} process: ${reason}`);
6393
+ }
6394
+ }, timeoutMs);
6395
+ }
6124
6396
  startCompactionWatchdog(agentId, ap) {
6125
6397
  this.clearCompactionWatchdog(ap);
6126
6398
  const startedAt = Date.now();
@@ -6218,8 +6490,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6218
6490
  runtime_profile_turn_outcome: control.kind === "migration" ? control.migrationDoneToolObserved ? "migration_done_observed" : "missing_migration_done" : "notice_only"
6219
6491
  };
6220
6492
  }
6221
- startRuntimeTrace(agentId, ap, reason, messages) {
6493
+ startRuntimeTrace(agentId, ap, reason, messages, inputTraceAttrs = {}) {
6222
6494
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
6495
+ ap.runtimeTraceCounters = createRuntimeTraceCounters();
6223
6496
  const messageControl = this.runtimeProfileTurnControlFromMessages(messages);
6224
6497
  if (messageControl) {
6225
6498
  this.activateRuntimeProfileTurnControl(ap, messageControl);
@@ -6234,12 +6507,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6234
6507
  reason,
6235
6508
  hasSession: Boolean(ap.sessionId),
6236
6509
  ...this.messagesTraceAttrs(messages),
6510
+ ...inputTraceAttrs,
6237
6511
  ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
6238
6512
  }
6239
6513
  });
6240
6514
  span.addEvent("daemon.turn.started", {
6241
6515
  reason,
6242
6516
  ...this.messagesTraceAttrs(messages),
6517
+ ...inputTraceAttrs,
6243
6518
  ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
6244
6519
  });
6245
6520
  ap.runtimeTraceSpan = span;
@@ -6279,7 +6554,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6279
6554
  tryFlushGatedSteering(agentId, ap, reason) {
6280
6555
  if (ap.driver.busyDeliveryMode !== "gated") return false;
6281
6556
  if (!ap.sessionId || ap.inbox.length === 0) return false;
6282
- if (reason === "tool_batch_complete") {
6557
+ if (reason !== "turn_end") {
6283
6558
  if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
6284
6559
  if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
6285
6560
  }
@@ -6288,7 +6563,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6288
6563
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
6289
6564
  ap.pendingNotificationCount = 0;
6290
6565
  ap.gatedSteering.lastFlushReason = reason;
6291
- if (reason === "tool_batch_complete") {
6566
+ if (reason !== "turn_end") {
6292
6567
  ap.gatedSteering.inFlightBatch = {
6293
6568
  reason,
6294
6569
  messages: nextMessages
@@ -6306,12 +6581,46 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6306
6581
  if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
6307
6582
  return true;
6308
6583
  }
6309
- if (reason === "tool_batch_complete") {
6584
+ if (reason !== "turn_end") {
6310
6585
  ap.gatedSteering.inFlightBatch = null;
6311
6586
  }
6312
6587
  ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
6313
6588
  return false;
6314
6589
  }
6590
+ flushCompactionBoundaryMessages(agentId, ap) {
6591
+ if (!ap.sessionId || !ap.driver.supportsStdinNotification || ap.inbox.length === 0) return false;
6592
+ if (ap.driver.busyDeliveryMode === "gated") {
6593
+ return this.tryFlushGatedSteering(agentId, ap, "compaction_finished");
6594
+ }
6595
+ if (ap.driver.busyDeliveryMode === "direct") {
6596
+ const pendingMessages = ap.inbox.length;
6597
+ const pendingNotificationCount = ap.pendingNotificationCount;
6598
+ const nextMessages = ap.inbox.splice(0, ap.inbox.length);
6599
+ ap.pendingNotificationCount = 0;
6600
+ if (ap.notificationTimer) {
6601
+ clearTimeout(ap.notificationTimer);
6602
+ ap.notificationTimer = null;
6603
+ }
6604
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.flush", {
6605
+ mode: "direct",
6606
+ messageCount: nextMessages.length
6607
+ });
6608
+ if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, "busy")) {
6609
+ return true;
6610
+ }
6611
+ ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
6612
+ return false;
6613
+ }
6614
+ if (ap.pendingNotificationCount > 0) {
6615
+ if (ap.notificationTimer) {
6616
+ clearTimeout(ap.notificationTimer);
6617
+ ap.notificationTimer = null;
6618
+ }
6619
+ this.sendStdinNotification(agentId);
6620
+ return true;
6621
+ }
6622
+ return false;
6623
+ }
6315
6624
  clearGatedInFlightBatch(agentId, ap, reason) {
6316
6625
  if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
6317
6626
  const messageCount = ap.gatedSteering.inFlightBatch.messages.length;
@@ -6348,6 +6657,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6348
6657
  lastActivity: ap.lastActivity,
6349
6658
  lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6350
6659
  lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6660
+ ...runtimeTraceCounterAttrs(ap),
6351
6661
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6352
6662
  });
6353
6663
  this.broadcastActivity(agentId, "error", diagnostic.detail);
@@ -6380,6 +6690,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6380
6690
  lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6381
6691
  pendingMessages: ap.inbox.length,
6382
6692
  recovery: "terminate_for_queued_message",
6693
+ ...runtimeTraceCounterAttrs(ap),
6383
6694
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6384
6695
  });
6385
6696
  ap.expectedTerminationReason = "stalled_recovery";
@@ -6394,8 +6705,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6394
6705
  expectedTerminationReason: "stalled_recovery",
6395
6706
  queued_messages_count: ap.inbox.length
6396
6707
  });
6708
+ this.startStalledRecoverySigtermWatchdog(agentId, ap, runtimeLabel, ap.inbox.length, staleForMs);
6397
6709
  ap.process.kill("SIGTERM");
6398
6710
  } catch (err) {
6711
+ this.clearStalledRecoverySigtermWatchdog(ap);
6399
6712
  const reason = err instanceof Error ? err.message : String(err);
6400
6713
  logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
6401
6714
  return false;
@@ -6407,6 +6720,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6407
6720
  const ap = this.agents.get(agentId);
6408
6721
  if (ap) {
6409
6722
  const wasStalled = Boolean(ap.runtimeProgressStaleSince);
6723
+ this.noteRuntimeTraceCounter(ap, event);
6410
6724
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.event.received", { kind: event.kind });
6411
6725
  if (wasStalled) {
6412
6726
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.observed", { afterStall: true });
@@ -6496,6 +6810,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6496
6810
  if (ap) {
6497
6811
  ap.gatedSteering.compacting = false;
6498
6812
  this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "compaction_finished" });
6813
+ this.flushCompactionBoundaryMessages(agentId, ap);
6499
6814
  ap.isIdle = false;
6500
6815
  }
6501
6816
  break;
@@ -6534,6 +6849,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6534
6849
  }
6535
6850
  this.endRuntimeTrace(ap, "ok", {
6536
6851
  outcome: "turn-completed",
6852
+ ...runtimeTraceCounterAttrs(ap),
6537
6853
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "turn_end")
6538
6854
  });
6539
6855
  if (ap.driver.terminateProcessOnTurnEnd) {
@@ -6575,10 +6891,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6575
6891
  `[Agent ${agentId}] Disabled Claude tool-boundary gated steering after thinking-block mutation error; lastFlushReason=${ap.gatedSteering.lastFlushReason || "none"}`
6576
6892
  );
6577
6893
  }
6578
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", runtimeErrorDiagnostics.eventAttrs);
6894
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", {
6895
+ ...runtimeErrorDiagnostics.eventAttrs,
6896
+ ...runtimeTraceCounterAttrs(ap)
6897
+ });
6579
6898
  this.endRuntimeTrace(ap, "error", {
6580
6899
  outcome: "runtime-error",
6581
6900
  ...runtimeErrorDiagnostics.spanAttrs,
6901
+ ...runtimeTraceCounterAttrs(ap),
6582
6902
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
6583
6903
  });
6584
6904
  if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
@@ -6601,6 +6921,29 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6601
6921
  sendAgentStatus(agentId, status, launchId) {
6602
6922
  this.sendToServer({ type: "agent:status", agentId, status, launchId: launchId || void 0 });
6603
6923
  }
6924
+ noteRuntimeTraceCounter(ap, event) {
6925
+ ap.runtimeTraceCounters.events++;
6926
+ switch (event.kind) {
6927
+ case "tool_call":
6928
+ ap.runtimeTraceCounters.toolCalls++;
6929
+ break;
6930
+ case "tool_output":
6931
+ ap.runtimeTraceCounters.toolOutputs++;
6932
+ break;
6933
+ case "compaction_started":
6934
+ ap.runtimeTraceCounters.compactionStarts++;
6935
+ break;
6936
+ case "compaction_finished":
6937
+ ap.runtimeTraceCounters.compactionFinishes++;
6938
+ break;
6939
+ case "text":
6940
+ ap.runtimeTraceCounters.textEvents++;
6941
+ break;
6942
+ case "thinking":
6943
+ ap.runtimeTraceCounters.thinkingEvents++;
6944
+ break;
6945
+ }
6946
+ }
6604
6947
  /** Send a batched notification to the agent via stdin about pending messages */
6605
6948
  sendStdinNotification(agentId) {
6606
6949
  const ap = this.agents.get(agentId);
@@ -6611,6 +6954,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6611
6954
  if (count === 0) return;
6612
6955
  if (ap.isIdle) return;
6613
6956
  if (!ap.sessionId) return;
6957
+ if (ap.gatedSteering.compacting) {
6958
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.delivery_suppressed", {
6959
+ pendingNotificationCount: count,
6960
+ pendingMessages: ap.inbox.length,
6961
+ busyDeliveryMode: ap.driver.busyDeliveryMode
6962
+ });
6963
+ ap.pendingNotificationCount += count;
6964
+ logger.info(
6965
+ `[Agent ${agentId}] Suppressing stdin delivery until context compaction finishes; pending=${ap.inbox.length}`
6966
+ );
6967
+ return;
6968
+ }
6614
6969
  if (ap.driver.busyDeliveryMode === "gated") {
6615
6970
  this.recordGatedSteeringEvent(agentId, ap, "suppress", {
6616
6971
  reason: "timer_notification_not_safe_boundary",
@@ -6703,6 +7058,14 @@ ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n"
6703
7058
 
6704
7059
  Respond as appropriate. Complete all your work before stopping.
6705
7060
  ${RESPONSE_TARGET_HINT}`);
7061
+ const inputTraceAttrs = buildRuntimeInputTraceAttrs({
7062
+ source: `stdin_${mode}_delivery`,
7063
+ prompt,
7064
+ messages,
7065
+ sessionIdPresent: Boolean(ap.sessionId),
7066
+ nativeStandingPrompt: Boolean(ap.driver.supportsNativeStandingPrompt)
7067
+ });
7068
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.input.prepared", inputTraceAttrs);
6706
7069
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
6707
7070
  if (!encoded) {
6708
7071
  ap.inbox.unshift(...messages);
@@ -6714,6 +7077,7 @@ ${RESPONSE_TARGET_HINT}`);
6714
7077
  );
6715
7078
  this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6716
7079
  ...traceAttrs,
7080
+ ...inputTraceAttrs,
6717
7081
  outcome: "encode_failed",
6718
7082
  requeued_messages_count: messages.length
6719
7083
  }, "error");
@@ -6730,6 +7094,7 @@ ${RESPONSE_TARGET_HINT}`);
6730
7094
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
6731
7095
  this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6732
7096
  ...traceAttrs,
7097
+ ...inputTraceAttrs,
6733
7098
  outcome: "written",
6734
7099
  stdin_write_attempted: true
6735
7100
  });
@@ -6751,8 +7116,8 @@ ${RESPONSE_TARGET_HINT}`);
6751
7116
  const nodes = [];
6752
7117
  for (const entry of entries) {
6753
7118
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
6754
- const fullPath = path11.join(dir, entry.name);
6755
- const relativePath = path11.relative(rootDir, fullPath);
7119
+ const fullPath = path12.join(dir, entry.name);
7120
+ const relativePath = path12.relative(rootDir, fullPath);
6756
7121
  let info;
6757
7122
  try {
6758
7123
  info = await stat2(fullPath);
@@ -7056,8 +7421,8 @@ var ReminderCache = class {
7056
7421
  // src/machineLock.ts
7057
7422
  import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
7058
7423
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
7059
- import os6 from "os";
7060
- import path12 from "path";
7424
+ import os7 from "os";
7425
+ import path13 from "path";
7061
7426
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
7062
7427
  var DaemonMachineLockConflictError = class extends Error {
7063
7428
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -7079,7 +7444,7 @@ function resolveDefaultMachineStateRoot() {
7079
7444
  return resolveSlockHomePath("machines");
7080
7445
  }
7081
7446
  function ownerPath(lockDir) {
7082
- return path12.join(lockDir, "owner.json");
7447
+ return path13.join(lockDir, "owner.json");
7083
7448
  }
7084
7449
  function readOwner(lockDir) {
7085
7450
  try {
@@ -7109,8 +7474,8 @@ function acquireDaemonMachineLock(options) {
7109
7474
  const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
7110
7475
  const fingerprint = apiKeyFingerprint(options.apiKey);
7111
7476
  const lockId = getDaemonMachineLockId(options.apiKey);
7112
- const machineDir = path12.join(rootDir, lockId);
7113
- const lockDir = path12.join(machineDir, "daemon.lock");
7477
+ const machineDir = path13.join(rootDir, lockId);
7478
+ const lockDir = path13.join(machineDir, "daemon.lock");
7114
7479
  const token = randomUUID2();
7115
7480
  mkdirSync5(machineDir, { recursive: true });
7116
7481
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -7119,7 +7484,7 @@ function acquireDaemonMachineLock(options) {
7119
7484
  const owner = {
7120
7485
  pid: process.pid,
7121
7486
  token,
7122
- hostname: os6.hostname(),
7487
+ hostname: os7.hostname(),
7123
7488
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7124
7489
  serverUrl: options.serverUrl,
7125
7490
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -7162,8 +7527,8 @@ function acquireDaemonMachineLock(options) {
7162
7527
  }
7163
7528
 
7164
7529
  // src/localTraceSink.ts
7165
- import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync4, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
7166
- import path13 from "path";
7530
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
7531
+ import path14 from "path";
7167
7532
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
7168
7533
  var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
7169
7534
  var DEFAULT_MAX_FILES = 8;
@@ -7199,7 +7564,7 @@ var LocalRotatingTraceSink = class {
7199
7564
  currentSize = 0;
7200
7565
  sequence = 0;
7201
7566
  constructor(options) {
7202
- this.traceDir = path13.join(options.machineDir, "traces");
7567
+ this.traceDir = path14.join(options.machineDir, "traces");
7203
7568
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
7204
7569
  const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
7205
7570
  const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
@@ -7229,7 +7594,7 @@ var LocalRotatingTraceSink = class {
7229
7594
  const nowMs = this.nowMsProvider();
7230
7595
  const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
7231
7596
  if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
7232
- this.currentFile = path13.join(
7597
+ this.currentFile = path14.join(
7233
7598
  this.traceDir,
7234
7599
  `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
7235
7600
  );
@@ -7240,11 +7605,11 @@ var LocalRotatingTraceSink = class {
7240
7605
  }
7241
7606
  }
7242
7607
  pruneOldFiles() {
7243
- const files = readdirSync4(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
7608
+ const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
7244
7609
  const excess = files.length - this.maxFiles;
7245
7610
  if (excess <= 0) return;
7246
7611
  for (const file of files.slice(0, excess)) {
7247
- rmSync3(path13.join(this.traceDir, file), { force: true });
7612
+ rmSync3(path14.join(this.traceDir, file), { force: true });
7248
7613
  }
7249
7614
  }
7250
7615
  };
@@ -7331,11 +7696,11 @@ function isDiagnosticErrorAttr(key) {
7331
7696
  import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
7332
7697
  import { gzipSync } from "zlib";
7333
7698
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
7334
- import path14 from "path";
7699
+ import path15 from "path";
7335
7700
 
7336
7701
  // src/directUploadCapability.ts
7337
- function joinUrl(base, path16) {
7338
- return `${base.replace(/\/+$/, "")}${path16}`;
7702
+ function joinUrl(base, path17) {
7703
+ return `${base.replace(/\/+$/, "")}${path17}`;
7339
7704
  }
7340
7705
  function jsonHeaders(apiKey) {
7341
7706
  return {
@@ -7554,7 +7919,7 @@ var DaemonTraceBundleUploader = class {
7554
7919
  }, nextMs);
7555
7920
  }
7556
7921
  async findUploadCandidates() {
7557
- const traceDir = path14.join(this.options.machineDir, "traces");
7922
+ const traceDir = path15.join(this.options.machineDir, "traces");
7558
7923
  let names;
7559
7924
  try {
7560
7925
  names = await readdir3(traceDir);
@@ -7566,8 +7931,8 @@ var DaemonTraceBundleUploader = class {
7566
7931
  const currentFile = this.options.currentFileProvider?.();
7567
7932
  const candidates = [];
7568
7933
  for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
7569
- const file = path14.join(traceDir, name);
7570
- if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
7934
+ const file = path15.join(traceDir, name);
7935
+ if (currentFile && path15.resolve(file) === path15.resolve(currentFile)) continue;
7571
7936
  if (await this.isUploaded(file)) continue;
7572
7937
  try {
7573
7938
  const info = await stat3(file);
@@ -7641,8 +8006,8 @@ var DaemonTraceBundleUploader = class {
7641
8006
  }
7642
8007
  }
7643
8008
  uploadStatePath(file) {
7644
- const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7645
- return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
8009
+ const stateDir = path15.join(this.options.machineDir, "trace-uploads");
8010
+ return path15.join(stateDir, `${path15.basename(file)}.uploaded.json`);
7646
8011
  }
7647
8012
  async isUploaded(file) {
7648
8013
  try {
@@ -7654,9 +8019,9 @@ var DaemonTraceBundleUploader = class {
7654
8019
  }
7655
8020
  async markUploaded(file, metadata) {
7656
8021
  const stateFile = this.uploadStatePath(file);
7657
- await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
8022
+ await mkdir2(path15.dirname(stateFile), { recursive: true, mode: 448 });
7658
8023
  await writeFile2(stateFile, `${JSON.stringify({
7659
- file: path14.basename(file),
8024
+ file: path15.basename(file),
7660
8025
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7661
8026
  ...metadata
7662
8027
  }, null, 2)}
@@ -7695,23 +8060,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
7695
8060
  }
7696
8061
  }
7697
8062
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
7698
- const dirname = path15.dirname(fileURLToPath(moduleUrl));
7699
- const jsPath = path15.resolve(dirname, "chat-bridge.js");
8063
+ const dirname = path16.dirname(fileURLToPath(moduleUrl));
8064
+ const jsPath = path16.resolve(dirname, "chat-bridge.js");
7700
8065
  try {
7701
8066
  accessSync(jsPath);
7702
8067
  return jsPath;
7703
8068
  } catch {
7704
- return path15.resolve(dirname, "chat-bridge.ts");
8069
+ return path16.resolve(dirname, "chat-bridge.ts");
7705
8070
  }
7706
8071
  }
7707
8072
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
7708
- const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7709
- const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
8073
+ const thisDir = path16.dirname(fileURLToPath(moduleUrl));
8074
+ const bundledDistPath = path16.resolve(thisDir, "cli", "index.js");
7710
8075
  try {
7711
8076
  accessSync(bundledDistPath);
7712
8077
  return bundledDistPath;
7713
8078
  } catch {
7714
- const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
8079
+ const workspaceDistPath = path16.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7715
8080
  accessSync(workspaceDistPath);
7716
8081
  return workspaceDistPath;
7717
8082
  }
@@ -7890,7 +8255,7 @@ var DaemonCore = class {
7890
8255
  }
7891
8256
  resolveMachineStateRoot() {
7892
8257
  if (this.options.machineStateDir) return this.options.machineStateDir;
7893
- if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
8258
+ if (this.options.dataDir) return path16.join(path16.dirname(this.options.dataDir), "machines");
7894
8259
  return resolveDefaultMachineStateRoot();
7895
8260
  }
7896
8261
  shouldEnableLocalTrace() {
@@ -8210,8 +8575,8 @@ var DaemonCore = class {
8210
8575
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
8211
8576
  runtimes,
8212
8577
  runningAgents: runningAgentIds,
8213
- hostname: this.options.hostname ?? os7.hostname(),
8214
- os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
8578
+ hostname: this.options.hostname ?? os8.hostname(),
8579
+ os: this.options.osDescription ?? `${os8.platform()} ${os8.arch()}`,
8215
8580
  daemonVersion: this.daemonVersion
8216
8581
  });
8217
8582
  this.recordDaemonTrace("daemon.ready.sent", {