@slock-ai/daemon 0.40.2 → 0.41.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,115 @@ import {
4
4
  } from "./chunk-JG7ONJZ6.js";
5
5
 
6
6
  // src/core.ts
7
- import path11 from "path";
8
- import os4 from "os";
7
+ import path12 from "path";
8
+ import os5 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
11
11
  import { fileURLToPath } from "url";
12
12
 
13
+ // ../shared/src/tracing/index.ts
14
+ var DEFAULT_TRACE_FLAGS = "00";
15
+ var TRACEPARENT_VERSION = "00";
16
+ var TRACE_ID_HEX_LENGTH = 32;
17
+ var SPAN_ID_HEX_LENGTH = 16;
18
+ var TRACE_FLAGS_HEX_LENGTH = 2;
19
+ var TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
20
+ var SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
21
+ var TRACE_FLAGS_PATTERN = /^[0-9a-f]{2}$/;
22
+ var TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
23
+ function isTraceId(value) {
24
+ return TRACE_ID_PATTERN.test(value) && value !== "0".repeat(TRACE_ID_HEX_LENGTH);
25
+ }
26
+ function isSpanId(value) {
27
+ return SPAN_ID_PATTERN.test(value) && value !== "0".repeat(SPAN_ID_HEX_LENGTH);
28
+ }
29
+ function isTraceFlags(value) {
30
+ return TRACE_FLAGS_PATTERN.test(value);
31
+ }
32
+ function assertTraceContext(context) {
33
+ if (!isTraceId(context.traceId)) {
34
+ throw new Error(`Invalid traceId: expected ${TRACE_ID_HEX_LENGTH} lowercase hex chars`);
35
+ }
36
+ if (!isSpanId(context.spanId)) {
37
+ throw new Error(`Invalid spanId: expected ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
38
+ }
39
+ if (context.parentSpanId !== null && !isSpanId(context.parentSpanId)) {
40
+ throw new Error(`Invalid parentSpanId: expected null or ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
41
+ }
42
+ if (!isTraceFlags(context.traceFlags)) {
43
+ throw new Error(`Invalid traceFlags: expected ${TRACE_FLAGS_HEX_LENGTH} lowercase hex chars`);
44
+ }
45
+ }
46
+ function formatTraceparent(context) {
47
+ assertTraceContext(context);
48
+ return `${TRACEPARENT_VERSION}-${context.traceId}-${context.spanId}-${context.traceFlags}`;
49
+ }
50
+ function parseTraceparent(value) {
51
+ if (!value) return null;
52
+ const match = TRACEPARENT_PATTERN.exec(value);
53
+ if (!match) return null;
54
+ const [, version, traceId, spanId, traceFlags] = match;
55
+ if (version !== TRACEPARENT_VERSION) return null;
56
+ if (!isTraceId(traceId) || !isSpanId(spanId) || !isTraceFlags(traceFlags)) return null;
57
+ return {
58
+ traceId,
59
+ spanId,
60
+ parentSpanId: null,
61
+ traceFlags
62
+ };
63
+ }
64
+ function createTraceContext({
65
+ parent = null,
66
+ traceId,
67
+ spanId,
68
+ traceFlags,
69
+ traceIdGenerator = generateTraceId,
70
+ spanIdGenerator = generateSpanId
71
+ } = {}) {
72
+ const context = {
73
+ traceId: traceId ?? parent?.traceId ?? traceIdGenerator(),
74
+ spanId: spanId ?? spanIdGenerator(),
75
+ parentSpanId: parent?.spanId ?? null,
76
+ traceFlags: traceFlags ?? parent?.traceFlags ?? DEFAULT_TRACE_FLAGS
77
+ };
78
+ assertTraceContext(context);
79
+ return context;
80
+ }
81
+ var NoopTracer = class {
82
+ startSpan(_name, options) {
83
+ return new NoopActiveSpan(createTraceContext({ parent: options.parent ?? null }));
84
+ }
85
+ };
86
+ var NoopActiveSpan = class {
87
+ context;
88
+ constructor(context) {
89
+ this.context = context;
90
+ }
91
+ addEvent() {
92
+ }
93
+ end() {
94
+ }
95
+ };
96
+ var noopTracer = new NoopTracer();
97
+ function generateTraceId() {
98
+ return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
99
+ }
100
+ function generateSpanId() {
101
+ return randomNonZeroHex(SPAN_ID_HEX_LENGTH);
102
+ }
103
+ function randomNonZeroHex(length) {
104
+ let value = randomHex(length);
105
+ while (value === "0".repeat(length)) {
106
+ value = randomHex(length);
107
+ }
108
+ return value;
109
+ }
110
+ function randomHex(length) {
111
+ const bytes = new Uint8Array(length / 2);
112
+ globalThis.crypto.getRandomValues(bytes);
113
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
114
+ }
115
+
13
116
  // ../shared/src/toolDisplay.ts
14
117
  var TOOL_DISPLAY_METADATA = {
15
118
  send_message: { logLabel: "Sending message", activityLabel: "Sending message\u2026", summaryKind: "message_target" },
@@ -272,6 +375,10 @@ function resolveSlockCliInvocation(toolName, input) {
272
375
  return { toolName: "search_messages", input: { query: readOptionValue(rest, "--query") } };
273
376
  case "server info":
274
377
  return { toolName: "list_server", input: {} };
378
+ case "channel members":
379
+ return { toolName: "list_channel_members", input: { channel: rest[0] } };
380
+ case "channel leave":
381
+ return { toolName: "leave_channel", input: { target: readOptionValue(rest, "--target") } };
275
382
  case "task list":
276
383
  return { toolName: "list_tasks", input: { channel: readOptionValue(rest, "--channel") } };
277
384
  case "task create":
@@ -481,6 +588,7 @@ import os3 from "os";
481
588
 
482
589
  // src/drivers/claude.ts
483
590
  import { spawn } from "child_process";
591
+ import { writeFileSync as writeFileSync2 } from "fs";
484
592
  import path3 from "path";
485
593
 
486
594
  // src/drivers/cliTransport.ts
@@ -533,20 +641,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
533
641
  1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
534
642
  2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
535
643
  3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
536
- 4. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
537
- 5. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
538
- 6. **\`slock task list\`** \u2014 View a channel's task board.
539
- 7. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
540
- 8. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
541
- 9. **\`slock task unclaim\`** \u2014 Release your claim on a task.
542
- 10. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
543
- 11. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
544
- 12. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
545
- 13. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
546
- 14. **\`slock reminder list\`** \u2014 List your reminders.
547
- 15. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
644
+ 4. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
645
+ 5. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
646
+ 6. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
647
+ 7. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
648
+ 8. **\`slock task list\`** \u2014 View a channel's task board.
649
+ 9. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
650
+ 10. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
651
+ 11. **\`slock task unclaim\`** \u2014 Release your claim on a task.
652
+ 12. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
653
+ 13. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
654
+ 14. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
655
+ 15. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
656
+ 16. **\`slock reminder list\`** \u2014 List your reminders.
657
+ 17. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
548
658
 
549
659
  When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
660
+ Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
550
661
  For agent-created reminders, first resolve the anchor message from the current conversation and pass its \`msgId\` explicitly. If you cannot resolve a message id, do not create the reminder.
551
662
 
552
663
  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:
@@ -562,15 +673,16 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
562
673
  1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
563
674
  2. **${sendCmd}** \u2014 Send a message to a channel or DM.
564
675
  3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
565
- 4. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
566
- 5. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
567
- 6. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
568
- 7. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
569
- 8. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
570
- 9. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
571
- 10. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
572
- 11. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
573
- 12. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
676
+ 4. **\`${t("leave_channel")}\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
677
+ 5. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
678
+ 6. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
679
+ 7. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
680
+ 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).
681
+ 9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
682
+ 10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
683
+ 11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
684
+ 12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
685
+ 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
574
686
  const sendingMessagesSection = isCli ? `### Sending messages
575
687
 
576
688
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
@@ -602,6 +714,7 @@ Threads are sub-conversations attached to a specific message. They let you discu
602
714
  - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`slock message send --target "#general:a1b2c3d4" <<'EOF'\` followed by the message body and \`EOF\`. The thread will be auto-created if it doesn't exist yet.
603
715
  - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
604
716
  - You can read thread history: \`slock message read --channel "#general:a1b2c3d4"\`
717
+ - You can stop receiving ordinary delivery for a thread with \`slock thread unfollow --target "#general:a1b2c3d4"\`. Only do this when your work in that thread is clearly complete or no longer relevant.
605
718
  - Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
606
719
 
607
720
  Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
@@ -615,10 +728,10 @@ Threads are sub-conversations attached to a specific message. They let you discu
615
728
  const discoverySection = isCli ? `### Discovering people and channels
616
729
 
617
730
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
618
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.` : `### Discovering people and channels
731
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
619
732
 
620
733
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
621
- 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.`;
734
+ 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")}\`.`;
622
735
  const channelAwarenessSection = isCli ? `### Channel awareness
623
736
 
624
737
  Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
@@ -962,6 +1075,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
962
1075
  ...ctx.config.envVars || {},
963
1076
  ...extraEnv,
964
1077
  SLOCK_AGENT_ID: ctx.agentId,
1078
+ ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
965
1079
  SLOCK_SERVER_URL: ctx.config.serverUrl,
966
1080
  SLOCK_AGENT_TOKEN_FILE: tokenFile,
967
1081
  PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
@@ -983,7 +1097,7 @@ function normalizeExecOutput(raw) {
983
1097
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
984
1098
  }
985
1099
  function resolveCommandOnWindows(command, env, execFileSyncFn) {
986
- const script = "$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition }";
1100
+ const script = "& {$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition } }";
987
1101
  try {
988
1102
  const output = normalizeExecOutput(execFileSyncFn("powershell.exe", [
989
1103
  "-NoProfile",
@@ -1049,9 +1163,12 @@ function resolveHomePath(relativePath, deps = {}) {
1049
1163
  // src/drivers/claude.ts
1050
1164
  var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
1051
1165
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1166
+ var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1167
+ var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
1052
1168
  var CLAUDE_DISALLOWED_TOOLS = [
1053
1169
  "EnterPlanMode",
1054
1170
  "ExitPlanMode",
1171
+ "ScheduleWakeup",
1055
1172
  "CronCreate",
1056
1173
  "CronList",
1057
1174
  "CronDelete"
@@ -1077,12 +1194,15 @@ var ClaudeDriver = class {
1077
1194
  id = "claude";
1078
1195
  supportsStdinNotification = true;
1079
1196
  mcpToolPrefix = "mcp__chat__";
1080
- busyDeliveryMode = "notification";
1197
+ // Claude Code supports same-turn steering, but raw stdin injection at an
1198
+ // arbitrary busy instant can collide with active signed thinking blocks. The
1199
+ // daemon therefore gates busy delivery on Claude stream-json boundaries.
1200
+ busyDeliveryMode = "gated";
1081
1201
  supportsNativeStandingPrompt = true;
1082
1202
  probe() {
1083
1203
  return probeClaude();
1084
1204
  }
1085
- buildClaudeArgs(config, standingPrompt) {
1205
+ buildClaudeArgs(config, standingPrompt, opts = {}) {
1086
1206
  const args = [
1087
1207
  "--allow-dangerously-skip-permissions",
1088
1208
  "--dangerously-skip-permissions",
@@ -1091,13 +1211,16 @@ var ClaudeDriver = class {
1091
1211
  "stream-json",
1092
1212
  "--input-format",
1093
1213
  "stream-json",
1094
- "--append-system-prompt",
1095
- standingPrompt,
1096
1214
  "--model",
1097
1215
  config.model || "sonnet",
1098
1216
  "--disallowed-tools",
1099
1217
  CLAUDE_DISALLOWED_TOOLS
1100
1218
  ];
1219
+ if (opts.standingPromptFilePath) {
1220
+ args.push("--append-system-prompt-file", opts.standingPromptFilePath);
1221
+ } else {
1222
+ args.push("--append-system-prompt", standingPrompt);
1223
+ }
1101
1224
  if (config.sessionId) {
1102
1225
  args.push("--resume", config.sessionId);
1103
1226
  }
@@ -1121,16 +1244,27 @@ var ClaudeDriver = class {
1121
1244
  ctx.config.authToken || ctx.daemonApiKey,
1122
1245
  "--runtime",
1123
1246
  this.id,
1247
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1124
1248
  "--deprecated-shim"
1125
1249
  ]
1126
1250
  }
1127
1251
  }
1128
1252
  });
1129
1253
  }
1254
+ writeClaudeLaunchFiles(ctx, slockDir) {
1255
+ const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1256
+ const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1257
+ writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1258
+ writeFileSync2(mcpConfigPath, this.buildDeprecatedShimMcpConfig(ctx), { mode: 384 });
1259
+ return { systemPromptPath, mcpConfigPath };
1260
+ }
1130
1261
  spawn(ctx) {
1131
- const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
1132
- const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt);
1133
- args.push("--mcp-config", this.buildDeprecatedShimMcpConfig(ctx));
1262
+ const { slockDir, tokenFile, spawnEnv } = prepareCliTransport(ctx);
1263
+ const { systemPromptPath, mcpConfigPath } = this.writeClaudeLaunchFiles(ctx, slockDir);
1264
+ const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt, {
1265
+ standingPromptFilePath: systemPromptPath
1266
+ });
1267
+ args.push("--mcp-config", mcpConfigPath);
1134
1268
  delete spawnEnv.CLAUDECODE;
1135
1269
  logger.info(
1136
1270
  `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
@@ -1200,6 +1334,17 @@ var ClaudeDriver = class {
1200
1334
  }
1201
1335
  break;
1202
1336
  }
1337
+ case "user": {
1338
+ const content = event.message?.content;
1339
+ if (Array.isArray(content)) {
1340
+ for (const block of content) {
1341
+ if (block.type === "tool_result") {
1342
+ events.push({ kind: "tool_output", name: block.name || block.tool_use_id || "tool_result" });
1343
+ }
1344
+ }
1345
+ }
1346
+ break;
1347
+ }
1203
1348
  case "result": {
1204
1349
  const subtype = typeof event.subtype === "string" ? event.subtype : "success";
1205
1350
  const stopReason = typeof event.stop_reason === "string" ? event.stop_reason : null;
@@ -1244,9 +1389,12 @@ var ClaudeDriver = class {
1244
1389
  return buildCliTransportSystemPrompt(config, {
1245
1390
  toolPrefix: "mcp__chat__",
1246
1391
  extraCriticalRules: [],
1247
- postStartupNotes: [],
1392
+ postStartupNotes: [
1393
+ "**Claude runtime note:** Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.",
1394
+ "For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
1395
+ ],
1248
1396
  includeStdinNotificationSection: true,
1249
- messageNotificationStyle: "poll"
1397
+ messageNotificationStyle: "direct"
1250
1398
  });
1251
1399
  }
1252
1400
  };
@@ -1364,6 +1512,7 @@ var CodexDriver = class {
1364
1512
  ctx.config.authToken || ctx.daemonApiKey,
1365
1513
  "--runtime",
1366
1514
  this.id,
1515
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1367
1516
  "--deprecated-shim"
1368
1517
  ] : [
1369
1518
  ctx.chatBridgePath,
@@ -1375,6 +1524,7 @@ var CodexDriver = class {
1375
1524
  ctx.config.authToken || ctx.daemonApiKey,
1376
1525
  "--runtime",
1377
1526
  this.id,
1527
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1378
1528
  "--deprecated-shim"
1379
1529
  ];
1380
1530
  return [
@@ -1574,6 +1724,9 @@ var CodexDriver = class {
1574
1724
  if (isStarted && typeof item.command === "string") {
1575
1725
  events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
1576
1726
  }
1727
+ if (isCompleted) {
1728
+ events.push({ kind: "tool_output", name: "shell" });
1729
+ }
1577
1730
  break;
1578
1731
  case "contextCompaction":
1579
1732
  if (isStarted) {
@@ -1599,6 +1752,10 @@ var CodexDriver = class {
1599
1752
  const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
1600
1753
  events.push({ kind: "tool_call", name: toolName, input: item.arguments });
1601
1754
  }
1755
+ if (isCompleted) {
1756
+ const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
1757
+ events.push({ kind: "tool_output", name: toolName });
1758
+ }
1602
1759
  break;
1603
1760
  case "collabAgentToolCall":
1604
1761
  if (isStarted) {
@@ -1749,7 +1906,7 @@ function detectCodexModels(home = os.homedir()) {
1749
1906
  // src/drivers/copilot.ts
1750
1907
  import { spawn as spawn3 } from "child_process";
1751
1908
  import path5 from "path";
1752
- import { writeFileSync as writeFileSync2 } from "fs";
1909
+ import { writeFileSync as writeFileSync3 } from "fs";
1753
1910
  var CopilotDriver = class {
1754
1911
  id = "copilot";
1755
1912
  supportsStdinNotification = false;
@@ -1764,7 +1921,7 @@ var CopilotDriver = class {
1764
1921
  const mcpCommand = isTsSource ? "npx" : "node";
1765
1922
  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];
1766
1923
  const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1767
- writeFileSync2(mcpConfigPath, JSON.stringify({
1924
+ writeFileSync3(mcpConfigPath, JSON.stringify({
1768
1925
  mcpServers: {
1769
1926
  chat: {
1770
1927
  command: mcpCommand,
@@ -1886,7 +2043,7 @@ var CopilotDriver = class {
1886
2043
 
1887
2044
  // src/drivers/cursor.ts
1888
2045
  import { spawn as spawn4 } from "child_process";
1889
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2046
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1890
2047
  import path6 from "path";
1891
2048
  var CursorDriver = class {
1892
2049
  id = "cursor";
@@ -1902,7 +2059,7 @@ var CursorDriver = class {
1902
2059
  const mcpCommand = isTsSource ? "npx" : "node";
1903
2060
  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];
1904
2061
  const mcpConfigPath = path6.join(cursorDir, "mcp.json");
1905
- writeFileSync3(mcpConfigPath, JSON.stringify({
2062
+ writeFileSync4(mcpConfigPath, JSON.stringify({
1906
2063
  mcpServers: {
1907
2064
  chat: {
1908
2065
  command: mcpCommand,
@@ -2006,7 +2163,7 @@ var CursorDriver = class {
2006
2163
 
2007
2164
  // src/drivers/gemini.ts
2008
2165
  import { spawn as spawn5 } from "child_process";
2009
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2166
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2010
2167
  import path7 from "path";
2011
2168
  var GeminiDriver = class {
2012
2169
  id = "gemini";
@@ -2026,7 +2183,7 @@ var GeminiDriver = class {
2026
2183
  const mcpCommand = isTsSource ? "npx" : "node";
2027
2184
  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];
2028
2185
  const settingsPath = path7.join(geminiDir, "settings.json");
2029
- writeFileSync4(settingsPath, JSON.stringify({
2186
+ writeFileSync5(settingsPath, JSON.stringify({
2030
2187
  mcpServers: {
2031
2188
  chat: {
2032
2189
  command: mcpCommand,
@@ -2122,7 +2279,7 @@ var GeminiDriver = class {
2122
2279
  // src/drivers/kimi.ts
2123
2280
  import { randomUUID } from "crypto";
2124
2281
  import { spawn as spawn6 } from "child_process";
2125
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
2282
+ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync6 } from "fs";
2126
2283
  import os2 from "os";
2127
2284
  import path8 from "path";
2128
2285
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
@@ -2145,6 +2302,21 @@ var KimiDriver = class {
2145
2302
  sessionId = null;
2146
2303
  sessionAnnounced = false;
2147
2304
  promptRequestId = null;
2305
+ buildChatBridgeArgs(ctx) {
2306
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2307
+ return [
2308
+ ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
2309
+ "--agent-id",
2310
+ ctx.agentId,
2311
+ "--server-url",
2312
+ ctx.config.serverUrl,
2313
+ "--auth-token",
2314
+ ctx.config.authToken || ctx.daemonApiKey,
2315
+ "--runtime",
2316
+ "kimi",
2317
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : []
2318
+ ];
2319
+ }
2148
2320
  spawn(ctx) {
2149
2321
  const isResume = !!ctx.config.sessionId;
2150
2322
  this.sessionId = ctx.config.sessionId || randomUUID();
@@ -2152,21 +2324,21 @@ var KimiDriver = class {
2152
2324
  this.promptRequestId = randomUUID();
2153
2325
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2154
2326
  const command = isTsSource ? "npx" : "node";
2155
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
2327
+ const bridgeArgs = this.buildChatBridgeArgs(ctx);
2156
2328
  const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2157
2329
  const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2158
2330
  const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
2159
2331
  if (!isResume || !existsSync5(systemPromptPath)) {
2160
- writeFileSync5(systemPromptPath, ctx.prompt, "utf8");
2332
+ writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
2161
2333
  }
2162
- writeFileSync5(agentFilePath, [
2334
+ writeFileSync6(agentFilePath, [
2163
2335
  "version: 1",
2164
2336
  "agent:",
2165
2337
  " extend: default",
2166
2338
  ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
2167
2339
  ""
2168
2340
  ].join("\n"), "utf8");
2169
- writeFileSync5(mcpConfigPath, JSON.stringify({
2341
+ writeFileSync6(mcpConfigPath, JSON.stringify({
2170
2342
  mcpServers: {
2171
2343
  chat: {
2172
2344
  command,
@@ -2539,10 +2711,13 @@ function buildUnreadSummary(messages, excludeChannel) {
2539
2711
  var MAX_TRAJECTORY_TEXT = 2e3;
2540
2712
  var TRAJECTORY_COALESCE_MS = 350;
2541
2713
  var ACTIVITY_HEARTBEAT_MS = 6e4;
2714
+ var COMPACTION_STALE_MS = 5 * 6e4;
2715
+ var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
2542
2716
  var MAX_STDOUT_LINES = 8;
2543
2717
  var MAX_STDOUT_LINE_LENGTH = 240;
2544
2718
  var MAX_STDERR_LINES = 8;
2545
2719
  var MAX_STDERR_LINE_LENGTH = 240;
2720
+ var MAX_GATED_STEERING_EVENTS = 12;
2546
2721
  var ONBOARDING_MEMORY_SEED_ENV = "SLOCK_ONBOARDING_MEMORY_SEED";
2547
2722
  var FIRST_CINDY_SEED_MODE = "first-cindy";
2548
2723
  function getOnboardingSeedMode(config) {
@@ -2853,6 +3028,31 @@ Success = user starts useful collaboration and setup progresses,
2853
3028
  not finishing a long onboarding conversation in one channel.
2854
3029
  `;
2855
3030
  }
3031
+ function createGatedSteeringState() {
3032
+ return {
3033
+ phase: "idle",
3034
+ outstandingToolUses: 0,
3035
+ compacting: false,
3036
+ toolBoundaryFlushDisabled: process.env.SLOCK_CLAUDE_GATED_STEERING_TOOL_BOUNDARY === "0",
3037
+ lastFlushReason: null,
3038
+ recentEvents: [],
3039
+ inFlightBatch: null
3040
+ };
3041
+ }
3042
+ var RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX = "runtime-profile-migration-";
3043
+ var RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX = "runtime-profile-daemon-release-";
3044
+ function runtimeProfileNotificationFromMessage(message) {
3045
+ if (message.message_id?.startsWith(RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX)) {
3046
+ return { kind: "migration", key: message.message_id.slice(RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX.length) };
3047
+ }
3048
+ if (message.message_id?.startsWith(RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX)) {
3049
+ return { kind: "daemon_release_notice", key: message.message_id.slice(RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX.length) };
3050
+ }
3051
+ return null;
3052
+ }
3053
+ function runtimeProfileNotificationTitle(kind) {
3054
+ return kind === "migration" ? "Runtime Profile migration" : "Runtime Profile notice";
3055
+ }
2856
3056
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
2857
3057
  const next = [...lines];
2858
3058
  for (const rawLine of chunk.split(/\r?\n/)) {
@@ -2924,6 +3124,9 @@ function getBusyDeliveryNote(driver) {
2924
3124
  if (driver.busyDeliveryMode === "direct") {
2925
3125
  return "\n\nNote: While you are busy, new messages may be delivered directly into your active turn. Handle them when appropriate and keep working.";
2926
3126
  }
3127
+ if (driver.busyDeliveryMode === "gated") {
3128
+ return "\n\nNote: While you are busy, new messages may be delivered at runtime-observed safe boundaries in your active turn. If no safe boundary is available, they will be delivered after the current turn ends.";
3129
+ }
2927
3130
  return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
2928
3131
  }
2929
3132
  var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
@@ -2942,6 +3145,7 @@ var AgentProcessManager = class _AgentProcessManager {
2942
3145
  dataDir;
2943
3146
  driverResolver;
2944
3147
  defaultAgentEnvVarsProvider;
3148
+ tracer;
2945
3149
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
2946
3150
  this.chatBridgePath = chatBridgePath;
2947
3151
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -2951,6 +3155,7 @@ var AgentProcessManager = class _AgentProcessManager {
2951
3155
  this.dataDir = opts.dataDir || DATA_DIR;
2952
3156
  this.driverResolver = opts.driverResolver || getDriver;
2953
3157
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3158
+ this.tracer = opts.tracer ?? noopTracer;
2954
3159
  }
2955
3160
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
2956
3161
  if (this.agents.has(agentId)) {
@@ -3043,7 +3248,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3043
3248
  workingDirectory: agentDataDir,
3044
3249
  chatBridgePath: this.chatBridgePath,
3045
3250
  slockCliPath: this.slockCliPath,
3046
- daemonApiKey: this.daemonApiKey
3251
+ daemonApiKey: this.daemonApiKey,
3252
+ launchId: launchId || null
3047
3253
  });
3048
3254
  const agentProcess = {
3049
3255
  process: proc,
@@ -3056,6 +3262,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3056
3262
  notificationTimer: null,
3057
3263
  pendingNotificationCount: 0,
3058
3264
  activityHeartbeat: null,
3265
+ compactionWatchdog: null,
3266
+ compactionStartedAt: null,
3267
+ lastRuntimeEventAt: Date.now(),
3268
+ runtimeProgressStaleSince: null,
3269
+ runtimeTraceSpan: null,
3059
3270
  lastActivity: "",
3060
3271
  lastActivityDetail: "",
3061
3272
  recentStdout: [],
@@ -3064,11 +3275,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3064
3275
  spawnError: null,
3065
3276
  exitCode: null,
3066
3277
  exitSignal: null,
3067
- pendingTrajectory: null
3278
+ pendingTrajectory: null,
3279
+ gatedSteering: createGatedSteeringState()
3068
3280
  };
3069
3281
  this.startingInboxes.delete(agentId);
3070
3282
  this.agents.set(agentId, agentProcess);
3283
+ this.startRuntimeTrace(agentId, agentProcess, "spawn");
3071
3284
  this.agentsStarting.delete(agentId);
3285
+ if (wakeMessage) {
3286
+ this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
3287
+ }
3072
3288
  let buffer = "";
3073
3289
  proc.stdout?.on("data", (chunk) => {
3074
3290
  const chunkText = chunk.toString();
@@ -3123,10 +3339,20 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3123
3339
  if (ap.activityHeartbeat) {
3124
3340
  clearInterval(ap.activityHeartbeat);
3125
3341
  }
3126
- this.agents.delete(agentId);
3127
3342
  const finalCode = ap.exitCode ?? code;
3128
3343
  const finalSignal = ap.exitSignal ?? signal;
3129
3344
  const terminalFailureDetail = classifyTerminalFailure(ap);
3345
+ this.endRuntimeTrace(ap, finalCode === 0 ? "ok" : "error", {
3346
+ outcome: finalCode === 0 ? "process-exit" : "process-crash",
3347
+ exitCode: finalCode,
3348
+ exitSignal: finalSignal
3349
+ });
3350
+ if (finalCode === 0) {
3351
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
3352
+ } else {
3353
+ this.clearCompactionWatchdog(ap);
3354
+ }
3355
+ this.agents.delete(agentId);
3130
3356
  if (finalCode === 0) {
3131
3357
  const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
3132
3358
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
@@ -3262,6 +3488,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3262
3488
  if (ap.activityHeartbeat) {
3263
3489
  clearInterval(ap.activityHeartbeat);
3264
3490
  }
3491
+ this.clearCompactionWatchdog(ap);
3265
3492
  this.agents.delete(agentId);
3266
3493
  ap.process.kill("SIGTERM");
3267
3494
  if (!silent) {
@@ -3315,6 +3542,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3315
3542
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
3316
3543
  nextMessages.push(message);
3317
3544
  ap.isIdle = false;
3545
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
3318
3546
  this.broadcastActivity(agentId, "working", "Message received");
3319
3547
  this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
3320
3548
  return;
@@ -3322,6 +3550,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3322
3550
  ap.inbox.push(message);
3323
3551
  if (!ap.driver.supportsStdinNotification) return;
3324
3552
  if (!ap.sessionId) return;
3553
+ if (ap.driver.busyDeliveryMode === "gated") {
3554
+ ap.pendingNotificationCount++;
3555
+ this.recordGatedSteeringEvent(agentId, ap, "buffer", {
3556
+ reason: "busy_message",
3557
+ pendingMessages: ap.inbox.length
3558
+ });
3559
+ return;
3560
+ }
3325
3561
  ap.pendingNotificationCount++;
3326
3562
  if (!ap.notificationTimer) {
3327
3563
  ap.notificationTimer = setTimeout(() => {
@@ -3359,6 +3595,127 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3359
3595
  }
3360
3596
  return result;
3361
3597
  }
3598
+ buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
3599
+ const workspacePath = path10.join(this.dataDir, agentId);
3600
+ return {
3601
+ agentId,
3602
+ launchId,
3603
+ facts: {
3604
+ runtime: config.runtime,
3605
+ model: config.model,
3606
+ reasoningEffort: config.reasoningEffort,
3607
+ executionMode: config.executionMode || "byoc",
3608
+ workspacePathRef: {
3609
+ label: "workspace",
3610
+ path: workspacePath,
3611
+ reachable: true
3612
+ },
3613
+ sessionRef: sessionId ? {
3614
+ label: sessionId,
3615
+ path: sessionId,
3616
+ runtime: config.runtime,
3617
+ reachable: true
3618
+ } : null
3619
+ }
3620
+ };
3621
+ }
3622
+ getAgentRuntimeProfileReport(agentId) {
3623
+ const running = this.agents.get(agentId);
3624
+ if (running) {
3625
+ return this.buildRuntimeProfileReport(agentId, running.config, running.sessionId, running.launchId);
3626
+ }
3627
+ const idle = this.idleAgentConfigs.get(agentId);
3628
+ if (idle) {
3629
+ return this.buildRuntimeProfileReport(agentId, idle.config, idle.sessionId, idle.launchId);
3630
+ }
3631
+ return null;
3632
+ }
3633
+ getAgentRuntimeProfileReports() {
3634
+ const reports = [];
3635
+ const seen = /* @__PURE__ */ new Set();
3636
+ for (const agentId of this.agents.keys()) {
3637
+ const report = this.getAgentRuntimeProfileReport(agentId);
3638
+ if (report) {
3639
+ reports.push(report);
3640
+ seen.add(agentId);
3641
+ }
3642
+ }
3643
+ for (const agentId of this.idleAgentConfigs.keys()) {
3644
+ if (seen.has(agentId)) continue;
3645
+ const report = this.getAgentRuntimeProfileReport(agentId);
3646
+ if (report) reports.push(report);
3647
+ }
3648
+ return reports;
3649
+ }
3650
+ deliverRuntimeProfileNotification(agentId, key, kind, content) {
3651
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3652
+ const message = {
3653
+ channel_id: "system",
3654
+ channel_name: "system",
3655
+ channel_type: "dm",
3656
+ sender_id: "system",
3657
+ sender_name: "system",
3658
+ sender_type: "system",
3659
+ content,
3660
+ timestamp: now,
3661
+ message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
3662
+ };
3663
+ const ap = this.agents.get(agentId);
3664
+ if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
3665
+ ap.isIdle = false;
3666
+ this.startRuntimeTrace(agentId, ap, "runtime-profile");
3667
+ this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
3668
+ return;
3669
+ }
3670
+ if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
3671
+ this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
3672
+ return;
3673
+ }
3674
+ const cached = this.idleAgentConfigs.get(agentId);
3675
+ if (cached) {
3676
+ logger.info(`[Agent ${agentId}] Starting from idle state for runtime profile ${kind} ${key}`);
3677
+ this.idleAgentConfigs.delete(agentId);
3678
+ this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
3679
+ logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
3680
+ this.idleAgentConfigs.set(agentId, cached);
3681
+ });
3682
+ return;
3683
+ }
3684
+ logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
3685
+ }
3686
+ ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
3687
+ for (const message of messages) {
3688
+ const notification = runtimeProfileNotificationFromMessage(message);
3689
+ if (!notification) continue;
3690
+ const title = runtimeProfileNotificationTitle(notification.kind);
3691
+ this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: message.content }], launchId);
3692
+ if (notification.kind === "migration") {
3693
+ this.sendToServer({
3694
+ type: "agent:runtime_profile:migration:ack",
3695
+ agentId,
3696
+ migrationKey: notification.key,
3697
+ launchId: launchId || void 0
3698
+ });
3699
+ } else {
3700
+ this.sendToServer({
3701
+ type: "agent:runtime_profile:daemon_release_notice:ack",
3702
+ agentId,
3703
+ noticeKey: notification.key,
3704
+ launchId: launchId || void 0
3705
+ });
3706
+ }
3707
+ }
3708
+ }
3709
+ sendRuntimeProfileReport(agentId) {
3710
+ const report = this.getAgentRuntimeProfileReport(agentId);
3711
+ if (!report) return;
3712
+ this.sendToServer({
3713
+ type: "agent:runtime_profile",
3714
+ agentId: report.agentId,
3715
+ facts: report.facts,
3716
+ launchId: report.launchId || void 0
3717
+ });
3718
+ }
3362
3719
  // Machine-level workspace scanning
3363
3720
  async scanAllWorkspaces() {
3364
3721
  return scanWorkspaceDirectories(this.dataDir);
@@ -3541,6 +3898,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3541
3898
  if (activity === "working" || activity === "thinking") {
3542
3899
  if (!ap.activityHeartbeat) {
3543
3900
  ap.activityHeartbeat = setInterval(() => {
3901
+ if (this.markRuntimeProgressStaleIfNeeded(agentId, ap)) return;
3902
+ this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
3903
+ activity: ap.lastActivity
3904
+ });
3544
3905
  this.sendToServer({
3545
3906
  type: "agent:activity",
3546
3907
  agentId,
@@ -3596,27 +3957,202 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3596
3957
  timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
3597
3958
  };
3598
3959
  }
3960
+ clearCompactionWatchdog(ap) {
3961
+ if (ap.compactionWatchdog) {
3962
+ clearTimeout(ap.compactionWatchdog);
3963
+ ap.compactionWatchdog = null;
3964
+ }
3965
+ ap.compactionStartedAt = null;
3966
+ }
3967
+ startCompactionWatchdog(agentId, ap) {
3968
+ this.clearCompactionWatchdog(ap);
3969
+ const startedAt = Date.now();
3970
+ ap.compactionStartedAt = startedAt;
3971
+ ap.compactionWatchdog = setTimeout(() => {
3972
+ this.markCompactionStale(agentId, startedAt);
3973
+ }, COMPACTION_STALE_MS);
3974
+ }
3975
+ markCompactionStale(agentId, startedAt) {
3976
+ const ap = this.agents.get(agentId);
3977
+ if (!ap || ap.compactionStartedAt !== startedAt) return;
3978
+ ap.compactionWatchdog = null;
3979
+ this.broadcastActivity(agentId, "working", "Context compaction still running; no finish event observed");
3980
+ }
3981
+ finishCompactionIfActive(agentId, detail = "Context compaction finished") {
3982
+ const ap = this.agents.get(agentId);
3983
+ if (!ap || !ap.compactionStartedAt) return;
3984
+ this.clearCompactionWatchdog(ap);
3985
+ this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
3986
+ }
3987
+ startRuntimeTrace(agentId, ap, reason) {
3988
+ if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
3989
+ const span = this.tracer.startSpan("daemon.runtime.turn", {
3990
+ surface: "daemon",
3991
+ kind: "internal",
3992
+ attrs: {
3993
+ agentId,
3994
+ runtime: ap.config.runtime,
3995
+ model: ap.config.model,
3996
+ reason,
3997
+ hasSession: Boolean(ap.sessionId)
3998
+ }
3999
+ });
4000
+ span.addEvent("daemon.turn.started", { reason });
4001
+ ap.runtimeTraceSpan = span;
4002
+ return span;
4003
+ }
4004
+ endRuntimeTrace(ap, status, attrs) {
4005
+ if (!ap.runtimeTraceSpan) return;
4006
+ ap.runtimeTraceSpan.end(status, attrs ? { attrs } : void 0);
4007
+ ap.runtimeTraceSpan = null;
4008
+ }
4009
+ recordRuntimeTraceEvent(agentId, ap, name, attrs) {
4010
+ this.startRuntimeTrace(agentId, ap, "runtime-progress").addEvent(name, attrs);
4011
+ }
4012
+ noteRuntimeProgress(ap) {
4013
+ ap.lastRuntimeEventAt = Date.now();
4014
+ ap.runtimeProgressStaleSince = null;
4015
+ }
4016
+ recordGatedSteeringEvent(agentId, ap, event, attrs = {}) {
4017
+ if (ap.driver.busyDeliveryMode !== "gated") return;
4018
+ const summary = `${event}:${ap.gatedSteering.phase}:tools=${ap.gatedSteering.outstandingToolUses}:compact=${ap.gatedSteering.compacting}`;
4019
+ ap.gatedSteering.recentEvents.push(summary);
4020
+ ap.gatedSteering.recentEvents = ap.gatedSteering.recentEvents.slice(-MAX_GATED_STEERING_EVENTS);
4021
+ this.recordRuntimeTraceEvent(agentId, ap, `runtime.gated_steering.${event}`, {
4022
+ phase: ap.gatedSteering.phase,
4023
+ outstandingToolUses: ap.gatedSteering.outstandingToolUses,
4024
+ compacting: ap.gatedSteering.compacting,
4025
+ toolBoundaryFlushDisabled: ap.gatedSteering.toolBoundaryFlushDisabled,
4026
+ pendingMessages: ap.inbox.length,
4027
+ ...attrs
4028
+ });
4029
+ }
4030
+ setGatedSteeringPhase(agentId, ap, phase, attrs = {}) {
4031
+ if (!ap || ap.driver.busyDeliveryMode !== "gated") return;
4032
+ ap.gatedSteering.phase = phase;
4033
+ this.recordGatedSteeringEvent(agentId, ap, "phase", attrs);
4034
+ }
4035
+ tryFlushGatedSteering(agentId, ap, reason) {
4036
+ if (ap.driver.busyDeliveryMode !== "gated") return false;
4037
+ if (!ap.sessionId || ap.inbox.length === 0) return false;
4038
+ if (reason === "tool_batch_complete") {
4039
+ if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
4040
+ if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
4041
+ }
4042
+ const pendingMessages = ap.inbox.length;
4043
+ const pendingNotificationCount = ap.pendingNotificationCount;
4044
+ const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4045
+ ap.pendingNotificationCount = 0;
4046
+ ap.gatedSteering.lastFlushReason = reason;
4047
+ if (reason === "tool_batch_complete") {
4048
+ ap.gatedSteering.inFlightBatch = {
4049
+ reason,
4050
+ messages: nextMessages
4051
+ };
4052
+ } else {
4053
+ ap.gatedSteering.inFlightBatch = null;
4054
+ }
4055
+ this.recordGatedSteeringEvent(agentId, ap, "flush", { reason, messageCount: nextMessages.length });
4056
+ logger.info(
4057
+ `[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
4058
+ );
4059
+ this.broadcastActivity(agentId, "working", "Message received");
4060
+ if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
4061
+ return true;
4062
+ }
4063
+ if (reason === "tool_batch_complete") {
4064
+ ap.gatedSteering.inFlightBatch = null;
4065
+ }
4066
+ ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
4067
+ return false;
4068
+ }
4069
+ clearGatedInFlightBatch(agentId, ap, reason) {
4070
+ if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
4071
+ const messageCount = ap.gatedSteering.inFlightBatch.messages.length;
4072
+ ap.gatedSteering.inFlightBatch = null;
4073
+ this.recordGatedSteeringEvent(agentId, ap, "ack", { reason, messageCount });
4074
+ }
4075
+ requeueGatedInFlightBatch(agentId, ap, reason) {
4076
+ if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
4077
+ const batch = ap.gatedSteering.inFlightBatch;
4078
+ ap.gatedSteering.inFlightBatch = null;
4079
+ ap.inbox.unshift(...batch.messages);
4080
+ ap.pendingNotificationCount += batch.messages.length;
4081
+ this.recordGatedSteeringEvent(agentId, ap, "requeue", {
4082
+ reason,
4083
+ originalFlushReason: batch.reason,
4084
+ messageCount: batch.messages.length
4085
+ });
4086
+ }
4087
+ isThinkingBlockMutationError(message) {
4088
+ return /thinking.*redacted_thinking|redacted_thinking.*thinking/i.test(message) && /cannot be modified/i.test(message);
4089
+ }
4090
+ markRuntimeProgressStaleIfNeeded(agentId, ap) {
4091
+ if (ap.lastActivity !== "working" && ap.lastActivity !== "thinking") return false;
4092
+ if (ap.runtimeProgressStaleSince) return true;
4093
+ const staleForMs = Date.now() - ap.lastRuntimeEventAt;
4094
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
4095
+ ap.runtimeProgressStaleSince = Date.now();
4096
+ const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
4097
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
4098
+ ageMs: staleForMs,
4099
+ staleForMinutes,
4100
+ lastActivity: ap.lastActivity
4101
+ });
4102
+ this.endRuntimeTrace(ap, "error", {
4103
+ outcome: "runtime-stalled",
4104
+ ageMs: staleForMs,
4105
+ lastActivity: ap.lastActivity
4106
+ });
4107
+ this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
4108
+ return true;
4109
+ }
3599
4110
  /** Handle a single ParsedEvent from any runtime driver */
3600
4111
  handleParsedEvent(agentId, event, driver) {
3601
4112
  const ap = this.agents.get(agentId);
4113
+ if (ap) {
4114
+ const wasStalled = Boolean(ap.runtimeProgressStaleSince);
4115
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.event.received", { kind: event.kind });
4116
+ if (wasStalled) {
4117
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.observed", { afterStall: true });
4118
+ }
4119
+ this.noteRuntimeProgress(ap);
4120
+ }
3602
4121
  switch (event.kind) {
3603
4122
  case "session_init":
3604
4123
  if (ap) ap.sessionId = event.sessionId;
3605
4124
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
4125
+ this.sendRuntimeProfileReport(agentId);
3606
4126
  break;
3607
4127
  case "thinking": {
4128
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
3608
4129
  this.queueTrajectoryText(agentId, "thinking", event.text);
3609
4130
  if (ap) ap.isIdle = false;
4131
+ if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
4132
+ this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "thinking" });
3610
4133
  break;
3611
4134
  }
3612
4135
  case "text": {
4136
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
3613
4137
  this.queueTrajectoryText(agentId, "text", event.text);
3614
4138
  if (ap) ap.isIdle = false;
4139
+ if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
4140
+ this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "text" });
3615
4141
  break;
3616
4142
  }
3617
4143
  case "tool_call": {
4144
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed tool use)");
3618
4145
  this.flushPendingTrajectory(agentId);
3619
4146
  const invocation = normalizeToolDisplayInvocation(event.name, event.input);
4147
+ if (ap) {
4148
+ ap.gatedSteering.outstandingToolUses++;
4149
+ this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
4150
+ this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
4151
+ this.setGatedSteeringPhase(agentId, ap, "tool_wait", {
4152
+ event: "tool_call",
4153
+ tool: invocation.toolName
4154
+ });
4155
+ }
3620
4156
  const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
3621
4157
  const detail = getToolActivityLabel(invocation.toolName);
3622
4158
  this.broadcastActivity(agentId, "working", detail, [{
@@ -3627,22 +4163,66 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3627
4163
  if (ap) ap.isIdle = false;
3628
4164
  break;
3629
4165
  }
4166
+ case "tool_output": {
4167
+ const invocation = normalizeToolDisplayInvocation(event.name, {});
4168
+ if (ap) {
4169
+ const hadOutstandingToolUse = ap.gatedSteering.outstandingToolUses > 0;
4170
+ ap.gatedSteering.outstandingToolUses = Math.max(0, ap.gatedSteering.outstandingToolUses - 1);
4171
+ this.recordRuntimeTraceEvent(agentId, ap, "tool.output.observed", { tool: invocation.toolName });
4172
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.continuation.expected");
4173
+ this.setGatedSteeringPhase(agentId, ap, "tool_boundary", {
4174
+ event: "tool_output",
4175
+ tool: invocation.toolName
4176
+ });
4177
+ ap.isIdle = false;
4178
+ if (hadOutstandingToolUse && ap.gatedSteering.outstandingToolUses === 0) {
4179
+ this.tryFlushGatedSteering(agentId, ap, "tool_batch_complete");
4180
+ }
4181
+ }
4182
+ break;
4183
+ }
3630
4184
  case "compaction_started":
3631
4185
  this.flushPendingTrajectory(agentId);
4186
+ if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.started");
4187
+ if (ap) this.startCompactionWatchdog(agentId, ap);
3632
4188
  this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
3633
- if (ap) ap.isIdle = false;
4189
+ if (ap) {
4190
+ ap.gatedSteering.compacting = true;
4191
+ this.setGatedSteeringPhase(agentId, ap, "compacting", { event: "compaction_started" });
4192
+ ap.isIdle = false;
4193
+ }
3634
4194
  break;
3635
4195
  case "compaction_finished":
3636
4196
  this.flushPendingTrajectory(agentId);
4197
+ if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.finished");
4198
+ if (ap) this.clearCompactionWatchdog(ap);
3637
4199
  this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
3638
- if (ap) ap.isIdle = false;
4200
+ if (ap) {
4201
+ ap.gatedSteering.compacting = false;
4202
+ this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "compaction_finished" });
4203
+ ap.isIdle = false;
4204
+ }
3639
4205
  break;
3640
4206
  case "turn_end":
4207
+ if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.turn.completed");
4208
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
3641
4209
  this.flushPendingTrajectory(agentId);
3642
4210
  if (ap) {
4211
+ this.clearGatedInFlightBatch(agentId, ap, "turn_end");
3643
4212
  if (event.sessionId) ap.sessionId = event.sessionId;
4213
+ ap.gatedSteering.outstandingToolUses = 0;
4214
+ ap.gatedSteering.compacting = false;
4215
+ this.setGatedSteeringPhase(agentId, ap, "idle", { event: "turn_end" });
3644
4216
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
3645
4217
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4218
+ ap.pendingNotificationCount = 0;
4219
+ if (ap.driver.busyDeliveryMode === "gated") {
4220
+ ap.gatedSteering.lastFlushReason = "turn_end";
4221
+ this.recordGatedSteeringEvent(agentId, ap, "flush", {
4222
+ reason: "turn_end",
4223
+ messageCount: nextMessages.length
4224
+ });
4225
+ }
3646
4226
  this.broadcastActivity(agentId, "working", "Message received");
3647
4227
  if (!this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle")) {
3648
4228
  ap.isIdle = true;
@@ -3652,14 +4232,34 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3652
4232
  ap.isIdle = true;
3653
4233
  this.broadcastActivity(agentId, "online", "Idle");
3654
4234
  }
4235
+ this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
3655
4236
  }
3656
4237
  if (event.sessionId) {
3657
4238
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
4239
+ this.sendRuntimeProfileReport(agentId);
3658
4240
  }
3659
4241
  break;
3660
4242
  case "error": {
4243
+ this.finishCompactionIfActive(agentId, "Context compaction interrupted by runtime error");
3661
4244
  this.flushPendingTrajectory(agentId);
3662
4245
  if (ap) ap.lastRuntimeError = event.message;
4246
+ if (ap) {
4247
+ this.setGatedSteeringPhase(agentId, ap, "error", { event: "error" });
4248
+ if (ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message)) {
4249
+ this.requeueGatedInFlightBatch(agentId, ap, "thinking_block_mutation_error");
4250
+ ap.gatedSteering.toolBoundaryFlushDisabled = true;
4251
+ this.recordGatedSteeringEvent(agentId, ap, "disabled", {
4252
+ reason: "thinking_block_mutation_error",
4253
+ lastFlushReason: ap.gatedSteering.lastFlushReason,
4254
+ recentEvents: ap.gatedSteering.recentEvents.join(" | ")
4255
+ });
4256
+ logger.warn(
4257
+ `[Agent ${agentId}] Disabled Claude tool-boundary gated steering after thinking-block mutation error; lastFlushReason=${ap.gatedSteering.lastFlushReason || "none"}`
4258
+ );
4259
+ }
4260
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
4261
+ this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
4262
+ }
3663
4263
  this.broadcastActivity(agentId, "error", event.message, [
3664
4264
  { kind: "text", text: `Error: ${event.message}` }
3665
4265
  ]);
@@ -3680,6 +4280,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3680
4280
  if (count === 0) return;
3681
4281
  if (ap.isIdle) return;
3682
4282
  if (!ap.sessionId) return;
4283
+ if (ap.driver.busyDeliveryMode === "gated") {
4284
+ this.recordGatedSteeringEvent(agentId, ap, "suppress", {
4285
+ reason: "timer_notification_not_safe_boundary",
4286
+ pendingNotificationCount: count
4287
+ });
4288
+ ap.pendingNotificationCount += count;
4289
+ logger.info(
4290
+ `[Agent ${agentId}] Suppressing raw busy stdin notification until Claude gated steering boundary; pending=${ap.inbox.length}`
4291
+ );
4292
+ return;
4293
+ }
3683
4294
  if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
3684
4295
  const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
3685
4296
  console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
@@ -3725,6 +4336,7 @@ Respond as appropriate. Complete all your work before stopping.`;
3725
4336
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
3726
4337
  );
3727
4338
  ap.process.stdin?.write(encoded + "\n");
4339
+ this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
3728
4340
  return true;
3729
4341
  }
3730
4342
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -3973,6 +4585,112 @@ var ReminderCache = class {
3973
4585
  }
3974
4586
  };
3975
4587
 
4588
+ // src/machineLock.ts
4589
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
4590
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync, writeFileSync as writeFileSync7 } from "fs";
4591
+ import os4 from "os";
4592
+ import path11 from "path";
4593
+ var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
4594
+ var INCOMPLETE_LOCK_STALE_MS = 3e4;
4595
+ var DaemonMachineLockConflictError = class extends Error {
4596
+ code = "DAEMON_MACHINE_LOCK_HELD";
4597
+ constructor(lockDir, owner) {
4598
+ const ownerText = owner ? `pid=${owner.pid}, startedAt=${owner.startedAt}, host=${owner.hostname}` : "unknown owner";
4599
+ super(
4600
+ `Another Slock daemon is already running for this machine key (${ownerText}). Lock: ${lockDir}. Stop the existing daemon first, or use a different machine key/state directory.`
4601
+ );
4602
+ this.name = "DaemonMachineLockConflictError";
4603
+ }
4604
+ };
4605
+ function apiKeyFingerprint(apiKey) {
4606
+ return createHash("sha256").update(apiKey).digest("hex");
4607
+ }
4608
+ function getDaemonMachineLockId(apiKey) {
4609
+ return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
4610
+ }
4611
+ function ownerPath(lockDir) {
4612
+ return path11.join(lockDir, "owner.json");
4613
+ }
4614
+ function readOwner(lockDir) {
4615
+ try {
4616
+ return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
4617
+ } catch {
4618
+ return null;
4619
+ }
4620
+ }
4621
+ function lockAgeMs(lockDir) {
4622
+ try {
4623
+ return Date.now() - statSync(lockDir).mtimeMs;
4624
+ } catch {
4625
+ return null;
4626
+ }
4627
+ }
4628
+ function isProcessAlive(pid) {
4629
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4630
+ try {
4631
+ process.kill(pid, 0);
4632
+ return true;
4633
+ } catch (err) {
4634
+ const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
4635
+ return code !== "ESRCH";
4636
+ }
4637
+ }
4638
+ function acquireDaemonMachineLock(options) {
4639
+ const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
4640
+ const fingerprint = apiKeyFingerprint(options.apiKey);
4641
+ const lockId = getDaemonMachineLockId(options.apiKey);
4642
+ const machineDir = path11.join(rootDir, lockId);
4643
+ const lockDir = path11.join(machineDir, "daemon.lock");
4644
+ const token = randomUUID2();
4645
+ mkdirSync4(machineDir, { recursive: true });
4646
+ for (let attempt = 0; attempt < 2; attempt += 1) {
4647
+ try {
4648
+ mkdirSync4(lockDir);
4649
+ const owner = {
4650
+ pid: process.pid,
4651
+ token,
4652
+ hostname: os4.hostname(),
4653
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4654
+ serverUrl: options.serverUrl,
4655
+ apiKeyFingerprint: fingerprint.slice(0, 16)
4656
+ };
4657
+ try {
4658
+ writeFileSync7(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
4659
+ `, { mode: 384 });
4660
+ } catch (err) {
4661
+ rmSync(lockDir, { recursive: true, force: true });
4662
+ throw err;
4663
+ }
4664
+ return {
4665
+ lockId,
4666
+ machineDir,
4667
+ lockDir,
4668
+ release: () => {
4669
+ const currentOwner = readOwner(lockDir);
4670
+ if (currentOwner?.pid === process.pid && currentOwner.token === token) {
4671
+ rmSync(lockDir, { recursive: true, force: true });
4672
+ }
4673
+ }
4674
+ };
4675
+ } catch (err) {
4676
+ const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
4677
+ if (code !== "EEXIST") throw err;
4678
+ const owner = readOwner(lockDir);
4679
+ if (owner?.pid && isProcessAlive(owner.pid)) {
4680
+ throw new DaemonMachineLockConflictError(lockDir, owner);
4681
+ }
4682
+ if (!owner) {
4683
+ const ageMs = lockAgeMs(lockDir);
4684
+ if (ageMs === null || ageMs < INCOMPLETE_LOCK_STALE_MS) {
4685
+ throw new DaemonMachineLockConflictError(lockDir, null);
4686
+ }
4687
+ }
4688
+ rmSync(lockDir, { recursive: true, force: true });
4689
+ }
4690
+ }
4691
+ throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
4692
+ }
4693
+
3976
4694
  // src/core.ts
3977
4695
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
3978
4696
  function parseDaemonCliArgs(args) {
@@ -3994,23 +4712,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
3994
4712
  }
3995
4713
  }
3996
4714
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
3997
- const dirname = path11.dirname(fileURLToPath(moduleUrl));
3998
- const jsPath = path11.resolve(dirname, "chat-bridge.js");
4715
+ const dirname = path12.dirname(fileURLToPath(moduleUrl));
4716
+ const jsPath = path12.resolve(dirname, "chat-bridge.js");
3999
4717
  try {
4000
4718
  accessSync(jsPath);
4001
4719
  return jsPath;
4002
4720
  } catch {
4003
- return path11.resolve(dirname, "chat-bridge.ts");
4721
+ return path12.resolve(dirname, "chat-bridge.ts");
4004
4722
  }
4005
4723
  }
4006
4724
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
4007
- const thisDir = path11.dirname(fileURLToPath(moduleUrl));
4008
- const bundledDistPath = path11.resolve(thisDir, "cli", "index.js");
4725
+ const thisDir = path12.dirname(fileURLToPath(moduleUrl));
4726
+ const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
4009
4727
  try {
4010
4728
  accessSync(bundledDistPath);
4011
4729
  return bundledDistPath;
4012
4730
  } catch {
4013
- const workspaceDistPath = path11.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
4731
+ const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
4014
4732
  accessSync(workspaceDistPath);
4015
4733
  return workspaceDistPath;
4016
4734
  }
@@ -4055,6 +4773,10 @@ function summarizeIncomingMessage(msg) {
4055
4773
  return `(agent=${msg.agentId})`;
4056
4774
  case "agent:deliver":
4057
4775
  return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
4776
+ case "agent:runtime_profile:migration":
4777
+ return `(agent=${msg.agentId}, migration=${msg.migrationKey})`;
4778
+ case "agent:runtime_profile:daemon_release_notice":
4779
+ return `(agent=${msg.agentId}, notice=${msg.noticeKey})`;
4058
4780
  case "agent:workspace:list":
4059
4781
  return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
4060
4782
  case "agent:workspace:read":
@@ -4084,12 +4806,15 @@ var DaemonCore = class {
4084
4806
  agentManager;
4085
4807
  connection;
4086
4808
  reminderCache;
4809
+ tracer;
4810
+ machineLock = null;
4087
4811
  constructor(options) {
4088
4812
  this.options = options;
4089
4813
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
4090
4814
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
4091
4815
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
4092
4816
  this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
4817
+ this.tracer = options.tracer ?? noopTracer;
4093
4818
  this.reminderCache = new ReminderCache({
4094
4819
  clock: options.reminderClock,
4095
4820
  onFire: (job) => this.onReminderFire(job)
@@ -4113,15 +4838,39 @@ var DaemonCore = class {
4113
4838
  });
4114
4839
  this.connection = connection;
4115
4840
  }
4841
+ resolveMachineStateRoot() {
4842
+ if (this.options.machineStateDir) return this.options.machineStateDir;
4843
+ if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
4844
+ return DEFAULT_MACHINE_STATE_ROOT;
4845
+ }
4116
4846
  start() {
4117
4847
  logger.info("[Slock Daemon] Starting...");
4118
- this.connection.connect();
4848
+ if (!this.machineLock) {
4849
+ this.machineLock = acquireDaemonMachineLock({
4850
+ apiKey: this.options.apiKey,
4851
+ serverUrl: this.options.serverUrl,
4852
+ rootDir: this.resolveMachineStateRoot()
4853
+ });
4854
+ logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
4855
+ }
4856
+ try {
4857
+ this.connection.connect();
4858
+ } catch (err) {
4859
+ this.machineLock.release();
4860
+ this.machineLock = null;
4861
+ throw err;
4862
+ }
4119
4863
  }
4120
4864
  async stop() {
4121
4865
  logger.info("[Slock Daemon] Shutting down...");
4122
4866
  this.reminderCache.clear();
4123
- await this.agentManager.stopAll();
4124
- this.connection.disconnect();
4867
+ try {
4868
+ await this.agentManager.stopAll();
4869
+ } finally {
4870
+ this.connection.disconnect();
4871
+ this.machineLock?.release();
4872
+ this.machineLock = null;
4873
+ }
4125
4874
  }
4126
4875
  get connected() {
4127
4876
  return this.connection.connected;
@@ -4150,10 +4899,44 @@ var DaemonCore = class {
4150
4899
  logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
4151
4900
  this.agentManager.resetWorkspace(msg.agentId);
4152
4901
  break;
4153
- case "agent:deliver":
4902
+ case "agent:deliver": {
4903
+ const parent = parseTraceparent(msg.traceparent);
4904
+ const span = this.tracer.startSpan("daemon.agent.delivery", {
4905
+ parent,
4906
+ surface: "daemon",
4907
+ kind: "consumer",
4908
+ attrs: {
4909
+ agentId: msg.agentId,
4910
+ seq: msg.seq
4911
+ }
4912
+ });
4154
4913
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
4155
- this.agentManager.deliverMessage(msg.agentId, msg.message);
4156
- this.connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
4914
+ try {
4915
+ span.addEvent("daemon.receive", { seq: msg.seq });
4916
+ this.agentManager.deliverMessage(msg.agentId, msg.message);
4917
+ span.addEvent("daemon.deliver_to_agent_manager");
4918
+ const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
4919
+ span.addEvent("daemon.ack.sent", { seq: ackSeq });
4920
+ this.connection.send({
4921
+ type: "agent:deliver:ack",
4922
+ agentId: msg.agentId,
4923
+ seq: ackSeq,
4924
+ traceparent: formatTraceparent(span.context)
4925
+ });
4926
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
4927
+ } catch (err) {
4928
+ span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
4929
+ throw err;
4930
+ }
4931
+ break;
4932
+ }
4933
+ case "agent:runtime_profile:migration":
4934
+ logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
4935
+ this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
4936
+ break;
4937
+ case "agent:runtime_profile:daemon_release_notice":
4938
+ logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
4939
+ this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
4157
4940
  break;
4158
4941
  case "agent:workspace:list":
4159
4942
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
@@ -4248,8 +5031,8 @@ var DaemonCore = class {
4248
5031
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
4249
5032
  runtimes,
4250
5033
  runningAgents: this.agentManager.getRunningAgentIds(),
4251
- hostname: this.options.hostname ?? os4.hostname(),
4252
- os: this.options.osDescription ?? `${os4.platform()} ${os4.arch()}`,
5034
+ hostname: this.options.hostname ?? os5.hostname(),
5035
+ os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
4253
5036
  daemonVersion: this.daemonVersion
4254
5037
  });
4255
5038
  for (const agentId of this.agentManager.getRunningAgentIds()) {
@@ -4262,6 +5045,14 @@ var DaemonCore = class {
4262
5045
  for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
4263
5046
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
4264
5047
  }
5048
+ for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
5049
+ this.connection.send({
5050
+ type: "agent:runtime_profile",
5051
+ agentId: report.agentId,
5052
+ facts: report.facts,
5053
+ launchId: report.launchId || void 0
5054
+ });
5055
+ }
4265
5056
  const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
4266
5057
  for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
4267
5058
  agentsForSnapshot.add(agentId);