@slock-ai/daemon 0.40.1 → 0.41.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,7 +1,7 @@
1
1
  import {
2
2
  buildWebSocketOptions,
3
3
  logger
4
- } from "./chunk-E6OOH3IC.js";
4
+ } from "./chunk-JG7ONJZ6.js";
5
5
 
6
6
  // src/core.ts
7
7
  import path11 from "path";
@@ -10,6 +10,109 @@ 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 ?? ""}`
@@ -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,21 +1211,60 @@ 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
  }
1104
1227
  return args;
1105
1228
  }
1229
+ buildDeprecatedShimMcpConfig(ctx) {
1230
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1231
+ const command = isTsSource ? "npx" : "node";
1232
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
1233
+ return JSON.stringify({
1234
+ mcpServers: {
1235
+ chat: {
1236
+ command,
1237
+ args: [
1238
+ ...bridgeArgs,
1239
+ "--agent-id",
1240
+ ctx.agentId,
1241
+ "--server-url",
1242
+ ctx.config.serverUrl,
1243
+ "--auth-token",
1244
+ ctx.config.authToken || ctx.daemonApiKey,
1245
+ "--runtime",
1246
+ this.id,
1247
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1248
+ "--deprecated-shim"
1249
+ ]
1250
+ }
1251
+ }
1252
+ });
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
+ }
1106
1261
  spawn(ctx) {
1107
- const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
1108
- const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt);
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);
1109
1268
  delete spawnEnv.CLAUDECODE;
1110
1269
  logger.info(
1111
1270
  `[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
@@ -1175,6 +1334,17 @@ var ClaudeDriver = class {
1175
1334
  }
1176
1335
  break;
1177
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
+ }
1178
1348
  case "result": {
1179
1349
  const subtype = typeof event.subtype === "string" ? event.subtype : "success";
1180
1350
  const stopReason = typeof event.stop_reason === "string" ? event.stop_reason : null;
@@ -1219,9 +1389,12 @@ var ClaudeDriver = class {
1219
1389
  return buildCliTransportSystemPrompt(config, {
1220
1390
  toolPrefix: "mcp__chat__",
1221
1391
  extraCriticalRules: [],
1222
- 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
+ ],
1223
1396
  includeStdinNotificationSection: true,
1224
- messageNotificationStyle: "poll"
1397
+ messageNotificationStyle: "direct"
1225
1398
  });
1226
1399
  }
1227
1400
  };
@@ -1325,6 +1498,50 @@ var CodexDriver = class {
1325
1498
  probe() {
1326
1499
  return probeCodex();
1327
1500
  }
1501
+ buildDeprecatedShimConfigArgs(ctx) {
1502
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1503
+ const command = isTsSource ? "npx" : "node";
1504
+ const bridgeArgs = isTsSource ? [
1505
+ "tsx",
1506
+ ctx.chatBridgePath,
1507
+ "--agent-id",
1508
+ ctx.agentId,
1509
+ "--server-url",
1510
+ ctx.config.serverUrl,
1511
+ "--auth-token",
1512
+ ctx.config.authToken || ctx.daemonApiKey,
1513
+ "--runtime",
1514
+ this.id,
1515
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1516
+ "--deprecated-shim"
1517
+ ] : [
1518
+ ctx.chatBridgePath,
1519
+ "--agent-id",
1520
+ ctx.agentId,
1521
+ "--server-url",
1522
+ ctx.config.serverUrl,
1523
+ "--auth-token",
1524
+ ctx.config.authToken || ctx.daemonApiKey,
1525
+ "--runtime",
1526
+ this.id,
1527
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1528
+ "--deprecated-shim"
1529
+ ];
1530
+ return [
1531
+ "-c",
1532
+ `mcp_servers.chat.command=${JSON.stringify(command)}`,
1533
+ "-c",
1534
+ `mcp_servers.chat.args=${JSON.stringify(bridgeArgs)}`,
1535
+ "-c",
1536
+ "mcp_servers.chat.startup_timeout_sec=30",
1537
+ "-c",
1538
+ "mcp_servers.chat.tool_timeout_sec=120",
1539
+ "-c",
1540
+ "mcp_servers.chat.enabled=true",
1541
+ "-c",
1542
+ "mcp_servers.chat.required=true"
1543
+ ];
1544
+ }
1328
1545
  buildThreadRequest(ctx) {
1329
1546
  const threadParams = {
1330
1547
  cwd: ctx.workingDirectory,
@@ -1378,6 +1595,7 @@ var CodexDriver = class {
1378
1595
  this.streamedAgentMessageIds.clear();
1379
1596
  this.streamedReasoningIds.clear();
1380
1597
  const args = ["app-server", "--listen", "stdio://"];
1598
+ args.push(...this.buildDeprecatedShimConfigArgs(ctx));
1381
1599
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
1382
1600
  const proc = spawn2(command, spawnArgs, {
1383
1601
  cwd: ctx.workingDirectory,
@@ -1506,6 +1724,9 @@ var CodexDriver = class {
1506
1724
  if (isStarted && typeof item.command === "string") {
1507
1725
  events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
1508
1726
  }
1727
+ if (isCompleted) {
1728
+ events.push({ kind: "tool_output", name: "shell" });
1729
+ }
1509
1730
  break;
1510
1731
  case "contextCompaction":
1511
1732
  if (isStarted) {
@@ -1531,6 +1752,10 @@ var CodexDriver = class {
1531
1752
  const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
1532
1753
  events.push({ kind: "tool_call", name: toolName, input: item.arguments });
1533
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
+ }
1534
1759
  break;
1535
1760
  case "collabAgentToolCall":
1536
1761
  if (isStarted) {
@@ -1681,7 +1906,7 @@ function detectCodexModels(home = os.homedir()) {
1681
1906
  // src/drivers/copilot.ts
1682
1907
  import { spawn as spawn3 } from "child_process";
1683
1908
  import path5 from "path";
1684
- import { writeFileSync as writeFileSync2 } from "fs";
1909
+ import { writeFileSync as writeFileSync3 } from "fs";
1685
1910
  var CopilotDriver = class {
1686
1911
  id = "copilot";
1687
1912
  supportsStdinNotification = false;
@@ -1696,7 +1921,7 @@ var CopilotDriver = class {
1696
1921
  const mcpCommand = isTsSource ? "npx" : "node";
1697
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];
1698
1923
  const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
1699
- writeFileSync2(mcpConfigPath, JSON.stringify({
1924
+ writeFileSync3(mcpConfigPath, JSON.stringify({
1700
1925
  mcpServers: {
1701
1926
  chat: {
1702
1927
  command: mcpCommand,
@@ -1818,7 +2043,7 @@ var CopilotDriver = class {
1818
2043
 
1819
2044
  // src/drivers/cursor.ts
1820
2045
  import { spawn as spawn4 } from "child_process";
1821
- 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";
1822
2047
  import path6 from "path";
1823
2048
  var CursorDriver = class {
1824
2049
  id = "cursor";
@@ -1834,7 +2059,7 @@ var CursorDriver = class {
1834
2059
  const mcpCommand = isTsSource ? "npx" : "node";
1835
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];
1836
2061
  const mcpConfigPath = path6.join(cursorDir, "mcp.json");
1837
- writeFileSync3(mcpConfigPath, JSON.stringify({
2062
+ writeFileSync4(mcpConfigPath, JSON.stringify({
1838
2063
  mcpServers: {
1839
2064
  chat: {
1840
2065
  command: mcpCommand,
@@ -1938,7 +2163,7 @@ var CursorDriver = class {
1938
2163
 
1939
2164
  // src/drivers/gemini.ts
1940
2165
  import { spawn as spawn5 } from "child_process";
1941
- 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";
1942
2167
  import path7 from "path";
1943
2168
  var GeminiDriver = class {
1944
2169
  id = "gemini";
@@ -1958,7 +2183,7 @@ var GeminiDriver = class {
1958
2183
  const mcpCommand = isTsSource ? "npx" : "node";
1959
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];
1960
2185
  const settingsPath = path7.join(geminiDir, "settings.json");
1961
- writeFileSync4(settingsPath, JSON.stringify({
2186
+ writeFileSync5(settingsPath, JSON.stringify({
1962
2187
  mcpServers: {
1963
2188
  chat: {
1964
2189
  command: mcpCommand,
@@ -2054,7 +2279,7 @@ var GeminiDriver = class {
2054
2279
  // src/drivers/kimi.ts
2055
2280
  import { randomUUID } from "crypto";
2056
2281
  import { spawn as spawn6 } from "child_process";
2057
- 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";
2058
2283
  import os2 from "os";
2059
2284
  import path8 from "path";
2060
2285
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
@@ -2077,6 +2302,21 @@ var KimiDriver = class {
2077
2302
  sessionId = null;
2078
2303
  sessionAnnounced = false;
2079
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
+ }
2080
2320
  spawn(ctx) {
2081
2321
  const isResume = !!ctx.config.sessionId;
2082
2322
  this.sessionId = ctx.config.sessionId || randomUUID();
@@ -2084,21 +2324,21 @@ var KimiDriver = class {
2084
2324
  this.promptRequestId = randomUUID();
2085
2325
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2086
2326
  const command = isTsSource ? "npx" : "node";
2087
- 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);
2088
2328
  const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2089
2329
  const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2090
2330
  const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
2091
2331
  if (!isResume || !existsSync5(systemPromptPath)) {
2092
- writeFileSync5(systemPromptPath, ctx.prompt, "utf8");
2332
+ writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
2093
2333
  }
2094
- writeFileSync5(agentFilePath, [
2334
+ writeFileSync6(agentFilePath, [
2095
2335
  "version: 1",
2096
2336
  "agent:",
2097
2337
  " extend: default",
2098
2338
  ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
2099
2339
  ""
2100
2340
  ].join("\n"), "utf8");
2101
- writeFileSync5(mcpConfigPath, JSON.stringify({
2341
+ writeFileSync6(mcpConfigPath, JSON.stringify({
2102
2342
  mcpServers: {
2103
2343
  chat: {
2104
2344
  command,
@@ -2471,10 +2711,13 @@ function buildUnreadSummary(messages, excludeChannel) {
2471
2711
  var MAX_TRAJECTORY_TEXT = 2e3;
2472
2712
  var TRAJECTORY_COALESCE_MS = 350;
2473
2713
  var ACTIVITY_HEARTBEAT_MS = 6e4;
2714
+ var COMPACTION_STALE_MS = 5 * 6e4;
2715
+ var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
2474
2716
  var MAX_STDOUT_LINES = 8;
2475
2717
  var MAX_STDOUT_LINE_LENGTH = 240;
2476
2718
  var MAX_STDERR_LINES = 8;
2477
2719
  var MAX_STDERR_LINE_LENGTH = 240;
2720
+ var MAX_GATED_STEERING_EVENTS = 12;
2478
2721
  var ONBOARDING_MEMORY_SEED_ENV = "SLOCK_ONBOARDING_MEMORY_SEED";
2479
2722
  var FIRST_CINDY_SEED_MODE = "first-cindy";
2480
2723
  function getOnboardingSeedMode(config) {
@@ -2785,6 +3028,31 @@ Success = user starts useful collaboration and setup progresses,
2785
3028
  not finishing a long onboarding conversation in one channel.
2786
3029
  `;
2787
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
+ }
2788
3056
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
2789
3057
  const next = [...lines];
2790
3058
  for (const rawLine of chunk.split(/\r?\n/)) {
@@ -2856,6 +3124,9 @@ function getBusyDeliveryNote(driver) {
2856
3124
  if (driver.busyDeliveryMode === "direct") {
2857
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.";
2858
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
+ }
2859
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.";
2860
3131
  }
2861
3132
  var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
@@ -2874,6 +3145,7 @@ var AgentProcessManager = class _AgentProcessManager {
2874
3145
  dataDir;
2875
3146
  driverResolver;
2876
3147
  defaultAgentEnvVarsProvider;
3148
+ tracer;
2877
3149
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
2878
3150
  this.chatBridgePath = chatBridgePath;
2879
3151
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -2883,6 +3155,7 @@ var AgentProcessManager = class _AgentProcessManager {
2883
3155
  this.dataDir = opts.dataDir || DATA_DIR;
2884
3156
  this.driverResolver = opts.driverResolver || getDriver;
2885
3157
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3158
+ this.tracer = opts.tracer ?? noopTracer;
2886
3159
  }
2887
3160
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
2888
3161
  if (this.agents.has(agentId)) {
@@ -2975,7 +3248,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2975
3248
  workingDirectory: agentDataDir,
2976
3249
  chatBridgePath: this.chatBridgePath,
2977
3250
  slockCliPath: this.slockCliPath,
2978
- daemonApiKey: this.daemonApiKey
3251
+ daemonApiKey: this.daemonApiKey,
3252
+ launchId: launchId || null
2979
3253
  });
2980
3254
  const agentProcess = {
2981
3255
  process: proc,
@@ -2988,6 +3262,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2988
3262
  notificationTimer: null,
2989
3263
  pendingNotificationCount: 0,
2990
3264
  activityHeartbeat: null,
3265
+ compactionWatchdog: null,
3266
+ compactionStartedAt: null,
3267
+ lastRuntimeEventAt: Date.now(),
3268
+ runtimeProgressStaleSince: null,
3269
+ runtimeTraceSpan: null,
2991
3270
  lastActivity: "",
2992
3271
  lastActivityDetail: "",
2993
3272
  recentStdout: [],
@@ -2996,11 +3275,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
2996
3275
  spawnError: null,
2997
3276
  exitCode: null,
2998
3277
  exitSignal: null,
2999
- pendingTrajectory: null
3278
+ pendingTrajectory: null,
3279
+ gatedSteering: createGatedSteeringState()
3000
3280
  };
3001
3281
  this.startingInboxes.delete(agentId);
3002
3282
  this.agents.set(agentId, agentProcess);
3283
+ this.startRuntimeTrace(agentId, agentProcess, "spawn");
3003
3284
  this.agentsStarting.delete(agentId);
3285
+ if (wakeMessage) {
3286
+ this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
3287
+ }
3004
3288
  let buffer = "";
3005
3289
  proc.stdout?.on("data", (chunk) => {
3006
3290
  const chunkText = chunk.toString();
@@ -3055,10 +3339,20 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3055
3339
  if (ap.activityHeartbeat) {
3056
3340
  clearInterval(ap.activityHeartbeat);
3057
3341
  }
3058
- this.agents.delete(agentId);
3059
3342
  const finalCode = ap.exitCode ?? code;
3060
3343
  const finalSignal = ap.exitSignal ?? signal;
3061
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);
3062
3356
  if (finalCode === 0) {
3063
3357
  const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
3064
3358
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
@@ -3194,6 +3488,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3194
3488
  if (ap.activityHeartbeat) {
3195
3489
  clearInterval(ap.activityHeartbeat);
3196
3490
  }
3491
+ this.clearCompactionWatchdog(ap);
3197
3492
  this.agents.delete(agentId);
3198
3493
  ap.process.kill("SIGTERM");
3199
3494
  if (!silent) {
@@ -3247,6 +3542,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3247
3542
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
3248
3543
  nextMessages.push(message);
3249
3544
  ap.isIdle = false;
3545
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
3250
3546
  this.broadcastActivity(agentId, "working", "Message received");
3251
3547
  this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
3252
3548
  return;
@@ -3254,6 +3550,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3254
3550
  ap.inbox.push(message);
3255
3551
  if (!ap.driver.supportsStdinNotification) return;
3256
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
+ }
3257
3561
  ap.pendingNotificationCount++;
3258
3562
  if (!ap.notificationTimer) {
3259
3563
  ap.notificationTimer = setTimeout(() => {
@@ -3291,6 +3595,127 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3291
3595
  }
3292
3596
  return result;
3293
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
+ }
3294
3719
  // Machine-level workspace scanning
3295
3720
  async scanAllWorkspaces() {
3296
3721
  return scanWorkspaceDirectories(this.dataDir);
@@ -3473,6 +3898,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3473
3898
  if (activity === "working" || activity === "thinking") {
3474
3899
  if (!ap.activityHeartbeat) {
3475
3900
  ap.activityHeartbeat = setInterval(() => {
3901
+ if (this.markRuntimeProgressStaleIfNeeded(agentId, ap)) return;
3902
+ this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
3903
+ activity: ap.lastActivity
3904
+ });
3476
3905
  this.sendToServer({
3477
3906
  type: "agent:activity",
3478
3907
  agentId,
@@ -3528,27 +3957,202 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3528
3957
  timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
3529
3958
  };
3530
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
+ }
3531
4110
  /** Handle a single ParsedEvent from any runtime driver */
3532
4111
  handleParsedEvent(agentId, event, driver) {
3533
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
+ }
3534
4121
  switch (event.kind) {
3535
4122
  case "session_init":
3536
4123
  if (ap) ap.sessionId = event.sessionId;
3537
4124
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
4125
+ this.sendRuntimeProfileReport(agentId);
3538
4126
  break;
3539
4127
  case "thinking": {
4128
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
3540
4129
  this.queueTrajectoryText(agentId, "thinking", event.text);
3541
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" });
3542
4133
  break;
3543
4134
  }
3544
4135
  case "text": {
4136
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
3545
4137
  this.queueTrajectoryText(agentId, "text", event.text);
3546
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" });
3547
4141
  break;
3548
4142
  }
3549
4143
  case "tool_call": {
4144
+ this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed tool use)");
3550
4145
  this.flushPendingTrajectory(agentId);
3551
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
+ }
3552
4156
  const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
3553
4157
  const detail = getToolActivityLabel(invocation.toolName);
3554
4158
  this.broadcastActivity(agentId, "working", detail, [{
@@ -3559,22 +4163,66 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3559
4163
  if (ap) ap.isIdle = false;
3560
4164
  break;
3561
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
+ }
3562
4184
  case "compaction_started":
3563
4185
  this.flushPendingTrajectory(agentId);
4186
+ if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.started");
4187
+ if (ap) this.startCompactionWatchdog(agentId, ap);
3564
4188
  this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
3565
- 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
+ }
3566
4194
  break;
3567
4195
  case "compaction_finished":
3568
4196
  this.flushPendingTrajectory(agentId);
4197
+ if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.finished");
4198
+ if (ap) this.clearCompactionWatchdog(ap);
3569
4199
  this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
3570
- 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
+ }
3571
4205
  break;
3572
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)");
3573
4209
  this.flushPendingTrajectory(agentId);
3574
4210
  if (ap) {
4211
+ this.clearGatedInFlightBatch(agentId, ap, "turn_end");
3575
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" });
3576
4216
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
3577
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
+ }
3578
4226
  this.broadcastActivity(agentId, "working", "Message received");
3579
4227
  if (!this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle")) {
3580
4228
  ap.isIdle = true;
@@ -3584,14 +4232,34 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3584
4232
  ap.isIdle = true;
3585
4233
  this.broadcastActivity(agentId, "online", "Idle");
3586
4234
  }
4235
+ this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
3587
4236
  }
3588
4237
  if (event.sessionId) {
3589
4238
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
4239
+ this.sendRuntimeProfileReport(agentId);
3590
4240
  }
3591
4241
  break;
3592
4242
  case "error": {
4243
+ this.finishCompactionIfActive(agentId, "Context compaction interrupted by runtime error");
3593
4244
  this.flushPendingTrajectory(agentId);
3594
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
+ }
3595
4263
  this.broadcastActivity(agentId, "error", event.message, [
3596
4264
  { kind: "text", text: `Error: ${event.message}` }
3597
4265
  ]);
@@ -3612,6 +4280,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3612
4280
  if (count === 0) return;
3613
4281
  if (ap.isIdle) return;
3614
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
+ }
3615
4294
  if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
3616
4295
  const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
3617
4296
  console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
@@ -3657,6 +4336,7 @@ Respond as appropriate. Complete all your work before stopping.`;
3657
4336
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
3658
4337
  );
3659
4338
  ap.process.stdin?.write(encoded + "\n");
4339
+ this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
3660
4340
  return true;
3661
4341
  }
3662
4342
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -3987,6 +4667,10 @@ function summarizeIncomingMessage(msg) {
3987
4667
  return `(agent=${msg.agentId})`;
3988
4668
  case "agent:deliver":
3989
4669
  return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
4670
+ case "agent:runtime_profile:migration":
4671
+ return `(agent=${msg.agentId}, migration=${msg.migrationKey})`;
4672
+ case "agent:runtime_profile:daemon_release_notice":
4673
+ return `(agent=${msg.agentId}, notice=${msg.noticeKey})`;
3990
4674
  case "agent:workspace:list":
3991
4675
  return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
3992
4676
  case "agent:workspace:read":
@@ -4016,12 +4700,14 @@ var DaemonCore = class {
4016
4700
  agentManager;
4017
4701
  connection;
4018
4702
  reminderCache;
4703
+ tracer;
4019
4704
  constructor(options) {
4020
4705
  this.options = options;
4021
4706
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
4022
4707
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
4023
4708
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
4024
4709
  this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
4710
+ this.tracer = options.tracer ?? noopTracer;
4025
4711
  this.reminderCache = new ReminderCache({
4026
4712
  clock: options.reminderClock,
4027
4713
  onFire: (job) => this.onReminderFire(job)
@@ -4082,10 +4768,44 @@ var DaemonCore = class {
4082
4768
  logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
4083
4769
  this.agentManager.resetWorkspace(msg.agentId);
4084
4770
  break;
4085
- case "agent:deliver":
4771
+ case "agent:deliver": {
4772
+ const parent = parseTraceparent(msg.traceparent);
4773
+ const span = this.tracer.startSpan("daemon.agent.delivery", {
4774
+ parent,
4775
+ surface: "daemon",
4776
+ kind: "consumer",
4777
+ attrs: {
4778
+ agentId: msg.agentId,
4779
+ seq: msg.seq
4780
+ }
4781
+ });
4086
4782
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
4087
- this.agentManager.deliverMessage(msg.agentId, msg.message);
4088
- this.connection.send({ type: "agent:deliver:ack", agentId: msg.agentId, seq: msg.seq });
4783
+ try {
4784
+ span.addEvent("daemon.receive", { seq: msg.seq });
4785
+ this.agentManager.deliverMessage(msg.agentId, msg.message);
4786
+ span.addEvent("daemon.deliver_to_agent_manager");
4787
+ const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
4788
+ span.addEvent("daemon.ack.sent", { seq: ackSeq });
4789
+ this.connection.send({
4790
+ type: "agent:deliver:ack",
4791
+ agentId: msg.agentId,
4792
+ seq: ackSeq,
4793
+ traceparent: formatTraceparent(span.context)
4794
+ });
4795
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
4796
+ } catch (err) {
4797
+ span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
4798
+ throw err;
4799
+ }
4800
+ break;
4801
+ }
4802
+ case "agent:runtime_profile:migration":
4803
+ logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
4804
+ this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
4805
+ break;
4806
+ case "agent:runtime_profile:daemon_release_notice":
4807
+ logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
4808
+ this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
4089
4809
  break;
4090
4810
  case "agent:workspace:list":
4091
4811
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
@@ -4194,6 +4914,14 @@ var DaemonCore = class {
4194
4914
  for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
4195
4915
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
4196
4916
  }
4917
+ for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
4918
+ this.connection.send({
4919
+ type: "agent:runtime_profile",
4920
+ agentId: report.agentId,
4921
+ facts: report.facts,
4922
+ launchId: report.launchId || void 0
4923
+ });
4924
+ }
4197
4925
  const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
4198
4926
  for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
4199
4927
  agentsForSnapshot.add(agentId);