@slock-ai/daemon 0.44.2 → 0.46.0-play.20260508161002

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,11 +1,14 @@
1
1
  import {
2
+ DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
2
3
  buildWebSocketOptions,
4
+ executeJsonRequest,
5
+ executeResponseRequest,
3
6
  logger
4
- } from "./chunk-JG7ONJZ6.js";
7
+ } from "./chunk-Z3PCMYZO.js";
5
8
 
6
9
  // src/core.ts
7
- import path13 from "path";
8
- import os6 from "os";
10
+ import path15 from "path";
11
+ import os7 from "os";
9
12
  import { createRequire } from "module";
10
13
  import { accessSync } from "fs";
11
14
  import { fileURLToPath } from "url";
@@ -94,6 +97,103 @@ var NoopActiveSpan = class {
94
97
  }
95
98
  };
96
99
  var noopTracer = new NoopTracer();
100
+ var BasicTracer = class {
101
+ sink;
102
+ clock;
103
+ traceIdGenerator;
104
+ spanIdGenerator;
105
+ constructor({
106
+ sink,
107
+ clock = () => Date.now(),
108
+ traceIdGenerator = generateTraceId,
109
+ spanIdGenerator = generateSpanId
110
+ }) {
111
+ this.sink = sink;
112
+ this.clock = clock;
113
+ this.traceIdGenerator = traceIdGenerator;
114
+ this.spanIdGenerator = spanIdGenerator;
115
+ }
116
+ startSpan(name, options) {
117
+ const startTimeMs = options.startTimeMs ?? this.clock();
118
+ const context = createTraceContext({
119
+ parent: options.parent ?? null,
120
+ traceIdGenerator: this.traceIdGenerator,
121
+ spanIdGenerator: this.spanIdGenerator
122
+ });
123
+ return new RecordingActiveSpan({
124
+ context,
125
+ name,
126
+ surface: options.surface,
127
+ kind: options.kind ?? "internal",
128
+ attrs: options.attrs,
129
+ startTimeMs,
130
+ clock: this.clock,
131
+ sink: this.sink
132
+ });
133
+ }
134
+ };
135
+ var RecordingActiveSpan = class {
136
+ context;
137
+ name;
138
+ surface;
139
+ kind;
140
+ startTimeMs;
141
+ clock;
142
+ sink;
143
+ events = [];
144
+ attrs;
145
+ ended = false;
146
+ constructor({
147
+ context,
148
+ name,
149
+ surface,
150
+ kind,
151
+ attrs,
152
+ startTimeMs,
153
+ clock,
154
+ sink
155
+ }) {
156
+ this.context = context;
157
+ this.name = name;
158
+ this.surface = surface;
159
+ this.kind = kind;
160
+ this.attrs = attrs;
161
+ this.startTimeMs = startTimeMs;
162
+ this.clock = clock;
163
+ this.sink = sink;
164
+ }
165
+ addEvent(name, attrs) {
166
+ if (this.ended) return;
167
+ this.events.push({
168
+ name,
169
+ timeMs: this.clock(),
170
+ ...attrs ? { attrs } : {}
171
+ });
172
+ }
173
+ end(status = "ok", options = {}) {
174
+ if (this.ended) return;
175
+ this.ended = true;
176
+ const endTimeMs = this.clock();
177
+ const attrs = mergeAttrs(this.attrs, options.attrs);
178
+ this.sink.record({
179
+ context: this.context,
180
+ name: this.name,
181
+ surface: this.surface,
182
+ kind: this.kind,
183
+ status,
184
+ startTimeMs: this.startTimeMs,
185
+ endTimeMs,
186
+ durationMs: Math.max(0, endTimeMs - this.startTimeMs),
187
+ ...attrs ? { attrs } : {},
188
+ events: [...this.events]
189
+ });
190
+ }
191
+ };
192
+ function mergeAttrs(base, extra) {
193
+ if (!base) return extra;
194
+ if (!extra) return base;
195
+ return { ...base, ...extra };
196
+ }
97
197
  function generateTraceId() {
98
198
  return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
99
199
  }
@@ -473,6 +573,9 @@ function summarizeToolInput(toolName, input) {
473
573
  }
474
574
  }
475
575
 
576
+ // ../shared/src/attachmentPreview.ts
577
+ var CSV_PREVIEW_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
578
+
476
579
  // ../shared/src/testing/failpoints.ts
477
580
  var NoopFailpointRegistry = class {
478
581
  get enabled() {
@@ -548,54 +651,6 @@ var RUNTIMES = [
548
651
  { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
549
652
  { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
550
653
  ];
551
- var RUNTIME_MODELS = {
552
- claude: [
553
- { id: "sonnet", label: "Sonnet" },
554
- { id: "opus", label: "Opus" },
555
- { id: "haiku", label: "Haiku" }
556
- ],
557
- codex: [
558
- { id: "gpt-5.5", label: "GPT-5.5" },
559
- { id: "gpt-5.4", label: "GPT-5.4" },
560
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
561
- { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
562
- { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
563
- { id: "gpt-5.2", label: "GPT-5.2" },
564
- { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
565
- { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
566
- { id: "gpt-5-codex", label: "GPT-5 Codex" },
567
- { id: "gpt-5", label: "GPT-5" }
568
- ],
569
- copilot: [
570
- { id: "gpt-5.4", label: "GPT-5.4" },
571
- { id: "gpt-5.2", label: "GPT-5.2" },
572
- { id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
573
- { id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
574
- ],
575
- cursor: [
576
- { id: "composer-2-fast", label: "Composer 2 Fast" },
577
- { id: "composer-2", label: "Composer 2" },
578
- { id: "auto", label: "Auto" }
579
- ],
580
- gemini: [
581
- { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
582
- { id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
583
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
584
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
585
- ],
586
- opencode: [
587
- { id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
588
- { id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
589
- { id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
590
- { id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
591
- { id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
592
- ],
593
- // Kimi CLI resolves model keys from each user's local config, so the safest
594
- // built-in option is to defer to whatever default model the CLI already uses.
595
- kimi: [
596
- { id: "default", label: "Configured Default" }
597
- ]
598
- };
599
654
  var PLAN_CONFIG = {
600
655
  free: {
601
656
  displayName: "Hobby",
@@ -631,14 +686,15 @@ var DISPLAY_PLAN_CONFIG = {
631
686
  };
632
687
 
633
688
  // src/agentProcessManager.ts
634
- import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
689
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
635
690
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
636
691
  import path11 from "path";
637
- import os4 from "os";
692
+ import os5 from "os";
638
693
 
639
694
  // src/drivers/claude.ts
640
695
  import { spawn } from "child_process";
641
- import { writeFileSync as writeFileSync2 } from "fs";
696
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
697
+ import os from "os";
642
698
  import path3 from "path";
643
699
 
644
700
  // src/drivers/cliTransport.ts
@@ -735,8 +791,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
735
791
  16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
736
792
  17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
737
793
  18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
738
- 19. **\`slock reminder list\`** \u2014 List your reminders.
739
- 20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
794
+ 19. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
795
+ 20. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
796
+ 21. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
797
+ 22. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
798
+ 23. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
740
799
 
741
800
  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:
742
801
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -759,22 +818,23 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
759
818
  9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
760
819
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
761
820
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
762
- 12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
821
+ 12. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
763
822
  13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
764
823
  14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
765
824
  15. **${listRemindersCmd}** \u2014 List your reminders.
766
825
  16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
767
826
  const reminderSection = `### Reminders
768
827
 
769
- Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
770
- Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
828
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
829
+ When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
830
+ Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
771
831
  Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
772
832
  const sendingMessagesSection = isCli ? `### Sending messages
773
833
 
774
834
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
775
- - **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
835
+ - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'EOF'\` followed by the message body and \`EOF\`
776
836
  - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
777
- - **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
837
+ - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'EOF'\` followed by the message body and \`EOF\`
778
838
 
779
839
  Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
780
840
  \`\`\`bash
@@ -814,10 +874,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
814
874
  const discoverySection = isCli ? `### Discovering people and channels
815
875
 
816
876
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
817
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
877
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.
878
+ Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
818
879
 
819
880
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
820
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
881
+ 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")}\`.
882
+ Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
821
883
  const channelAwarenessSection = isCli ? `### Channel awareness
822
884
 
823
885
  Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
@@ -831,7 +893,7 @@ Each channel has a **name** and optionally a **description** that define its pur
831
893
  - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
832
894
  const readingHistorySection = isCli ? `### Reading history
833
895
 
834
- \`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
896
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
835
897
 
836
898
  To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
837
899
 
@@ -923,7 +985,7 @@ ${readCmd} shows messages in their current state. If a message was later convert
923
985
  - Reuse existing tasks and threads instead of creating duplicates.
924
986
  - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
925
987
  const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
926
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
988
+ let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.
927
989
 
928
990
  ## Who you are
929
991
 
@@ -1031,6 +1093,17 @@ Keep the user informed. They cannot see your internal reasoning, so:
1031
1093
  - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1032
1094
  - When done, summarize the result.
1033
1095
  - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
1096
+ - For long answers where users need the conclusion first but details still matter, put the conclusion and next action outside any collapse, then use sanitized HTML details blocks for optional depth:
1097
+
1098
+ \`\`\`html
1099
+ <details>
1100
+ <summary>Evidence, logs, or edge cases</summary>
1101
+
1102
+ Detailed notes go here.
1103
+ </details>
1104
+ \`\`\`
1105
+
1106
+ Do not hide the main recommendation, blocker, or required action inside \`<details>\`; only fold supporting evidence, logs, alternatives, or extended rationale.
1034
1107
 
1035
1108
  ### Conversation etiquette
1036
1109
 
@@ -1301,6 +1374,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
1301
1374
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1302
1375
  var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1303
1376
  var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
1377
+ var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
1304
1378
  var CLAUDE_DISALLOWED_TOOLS = [
1305
1379
  "EnterPlanMode",
1306
1380
  "ExitPlanMode",
@@ -1326,6 +1400,80 @@ function probeClaude(deps = {}) {
1326
1400
  version: readCommandVersion(command, [], deps) ?? void 0
1327
1401
  };
1328
1402
  }
1403
+ function isRecord(value) {
1404
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1405
+ }
1406
+ function expandClaudeMcpConfigVariables(raw, vars) {
1407
+ let expanded = raw;
1408
+ for (const [name, value] of Object.entries(vars)) {
1409
+ expanded = expanded.replaceAll(`\${${name}}`, value);
1410
+ }
1411
+ return expanded;
1412
+ }
1413
+ function readClaudeMcpServers(configPath, vars = {}) {
1414
+ try {
1415
+ const parsed = JSON.parse(
1416
+ expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
1417
+ );
1418
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
1419
+ return parsed.mcpServers;
1420
+ } catch (err) {
1421
+ logger.warn(
1422
+ `[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
1423
+ );
1424
+ return null;
1425
+ }
1426
+ }
1427
+ function resolveClaudeConfigDir(ctx, home) {
1428
+ const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
1429
+ return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
1430
+ }
1431
+ function collectClaudeMcpConfigFiles(ctx, home) {
1432
+ const files = [];
1433
+ const pushIfFile = (candidate) => {
1434
+ try {
1435
+ if (existsSync2(candidate) && statSync(candidate).isFile()) {
1436
+ files.push({ path: candidate });
1437
+ }
1438
+ } catch {
1439
+ }
1440
+ };
1441
+ pushIfFile(path3.join(home, ".claude.json"));
1442
+ pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
1443
+ const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
1444
+ try {
1445
+ for (const entry of readdirSync(pluginRoot)) {
1446
+ const pluginPath = path3.join(pluginRoot, entry);
1447
+ const configPath = path3.join(pluginPath, ".mcp.json");
1448
+ try {
1449
+ if (existsSync2(configPath) && statSync(configPath).isFile()) {
1450
+ files.push({
1451
+ path: configPath,
1452
+ vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
1453
+ });
1454
+ }
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ } catch {
1459
+ }
1460
+ return files;
1461
+ }
1462
+ function buildClaudeUserMcpServers(ctx, home) {
1463
+ const servers = /* @__PURE__ */ Object.create(null);
1464
+ for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
1465
+ const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
1466
+ if (!mcpServers) continue;
1467
+ for (const [name, server] of Object.entries(mcpServers)) {
1468
+ if (!isRecord(server)) {
1469
+ logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
1470
+ continue;
1471
+ }
1472
+ servers[name] = server;
1473
+ }
1474
+ }
1475
+ return servers;
1476
+ }
1329
1477
  var ClaudeDriver = class {
1330
1478
  id = "claude";
1331
1479
  lifecycle = {
@@ -1381,36 +1529,46 @@ var ClaudeDriver = class {
1381
1529
  }
1382
1530
  return args;
1383
1531
  }
1384
- buildRuntimeActionsMcpConfig(ctx) {
1532
+ buildRuntimeActionsMcpServer(ctx) {
1385
1533
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1386
1534
  const command = isTsSource ? "npx" : "node";
1387
1535
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
1536
+ return {
1537
+ command,
1538
+ args: [
1539
+ ...bridgeArgs,
1540
+ "--agent-id",
1541
+ ctx.agentId,
1542
+ "--server-url",
1543
+ ctx.config.serverUrl,
1544
+ "--auth-token",
1545
+ ctx.config.authToken || ctx.daemonApiKey,
1546
+ "--runtime",
1547
+ this.id,
1548
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1549
+ "--runtime-actions-only"
1550
+ ]
1551
+ };
1552
+ }
1553
+ buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
1554
+ const userMcpServers = buildClaudeUserMcpServers(ctx, home);
1555
+ if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
1556
+ logger.warn(
1557
+ `[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
1558
+ );
1559
+ }
1388
1560
  return JSON.stringify({
1389
1561
  mcpServers: {
1390
- chat: {
1391
- command,
1392
- args: [
1393
- ...bridgeArgs,
1394
- "--agent-id",
1395
- ctx.agentId,
1396
- "--server-url",
1397
- ctx.config.serverUrl,
1398
- "--auth-token",
1399
- ctx.config.authToken || ctx.daemonApiKey,
1400
- "--runtime",
1401
- this.id,
1402
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1403
- "--runtime-actions-only"
1404
- ]
1405
- }
1562
+ ...userMcpServers,
1563
+ [SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
1406
1564
  }
1407
1565
  });
1408
1566
  }
1409
- writeClaudeLaunchFiles(ctx, slockDir) {
1567
+ writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
1410
1568
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1411
1569
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1412
1570
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1413
- writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1571
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
1414
1572
  return { systemPromptPath, mcpConfigPath };
1415
1573
  }
1416
1574
  spawn(ctx) {
@@ -1558,8 +1716,8 @@ var ClaudeDriver = class {
1558
1716
 
1559
1717
  // src/drivers/codex.ts
1560
1718
  import { spawn as spawn2, execSync } from "child_process";
1561
- import { existsSync as existsSync2, readFileSync } from "fs";
1562
- import os from "os";
1719
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1720
+ import os2 from "os";
1563
1721
  import path4 from "path";
1564
1722
  function getCodexNotificationErrorMessage(params) {
1565
1723
  const topLevelMessage = params?.message;
@@ -1573,7 +1731,7 @@ function getCodexNotificationErrorMessage(params) {
1573
1731
  return null;
1574
1732
  }
1575
1733
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1576
- const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1734
+ const existsSyncFn = deps.existsSyncFn ?? existsSync3;
1577
1735
  const execSyncFn = deps.execSyncFn ?? execSync;
1578
1736
  const gitDir = path4.join(workingDirectory, ".git");
1579
1737
  if (existsSyncFn(gitDir)) return;
@@ -1620,14 +1778,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1620
1778
  try {
1621
1779
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1622
1780
  const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1623
- if (existsSync2(candidate)) codexEntry = candidate;
1781
+ if (existsSync3(candidate)) codexEntry = candidate;
1624
1782
  } catch {
1625
1783
  }
1626
1784
  if (!codexEntry) {
1627
1785
  try {
1628
1786
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
1629
1787
  const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1630
- if (existsSync2(candidate)) codexEntry = candidate;
1788
+ if (existsSync3(candidate)) codexEntry = candidate;
1631
1789
  } catch {
1632
1790
  }
1633
1791
  }
@@ -2047,12 +2205,12 @@ var CodexDriver = class {
2047
2205
  return detectCodexModels();
2048
2206
  }
2049
2207
  };
2050
- function detectCodexModels(home = os.homedir()) {
2208
+ function detectCodexModels(home = os2.homedir()) {
2051
2209
  const cachePath = path4.join(home, ".codex", "models_cache.json");
2052
2210
  const configPath = path4.join(home, ".codex", "config.toml");
2053
2211
  let models = [];
2054
2212
  try {
2055
- const raw = readFileSync(cachePath, "utf8");
2213
+ const raw = readFileSync2(cachePath, "utf8");
2056
2214
  const parsed = JSON.parse(raw);
2057
2215
  const entries = Array.isArray(parsed?.models) ? parsed.models : [];
2058
2216
  for (const entry of entries) {
@@ -2069,7 +2227,7 @@ function detectCodexModels(home = os.homedir()) {
2069
2227
  if (models.length === 0) return null;
2070
2228
  let defaultModel;
2071
2229
  try {
2072
- const raw = readFileSync(configPath, "utf8");
2230
+ const raw = readFileSync2(configPath, "utf8");
2073
2231
  const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
2074
2232
  if (match) defaultModel = match[1];
2075
2233
  } catch {
@@ -2234,7 +2392,7 @@ var CopilotDriver = class {
2234
2392
 
2235
2393
  // src/drivers/cursor.ts
2236
2394
  import { spawn as spawn4, spawnSync } from "child_process";
2237
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2395
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2238
2396
  import path6 from "path";
2239
2397
  var CursorDriver = class {
2240
2398
  id = "cursor";
@@ -2260,7 +2418,7 @@ var CursorDriver = class {
2260
2418
  busyDeliveryMode = "none";
2261
2419
  spawn(ctx) {
2262
2420
  const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
2263
- if (!existsSync3(cursorDir)) {
2421
+ if (!existsSync4(cursorDir)) {
2264
2422
  mkdirSync2(cursorDir, { recursive: true });
2265
2423
  }
2266
2424
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
@@ -2412,18 +2570,14 @@ function runCursorModelsCommand() {
2412
2570
 
2413
2571
  // src/drivers/gemini.ts
2414
2572
  import { spawn as spawn5 } from "child_process";
2415
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2573
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2416
2574
  import path7 from "path";
2417
- function buildGeminiSpawnEnv(ctx) {
2418
- return {
2419
- ...process.env,
2420
- FORCE_COLOR: "0",
2421
- NO_COLOR: "1",
2422
- // Gemini CLI's trusted-workspace gate breaks our managed headless flow
2423
- // unless we explicitly trust the daemon-owned agent workspace.
2424
- GEMINI_CLI_TRUST_WORKSPACE: "true",
2425
- ...ctx.config.envVars || {}
2426
- };
2575
+ function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2576
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
2577
+ if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
2578
+ spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
2579
+ }
2580
+ return spawnEnv;
2427
2581
  }
2428
2582
  var GeminiDriver = class {
2429
2583
  id = "gemini";
@@ -2434,40 +2588,26 @@ var GeminiDriver = class {
2434
2588
  inFlightWake: "spawn_new"
2435
2589
  };
2436
2590
  communication = {
2437
- chat: "mcp_chat_bridge",
2591
+ chat: "slock_cli",
2438
2592
  runtimeControl: "mcp_runtime_actions"
2439
2593
  };
2440
2594
  session = {
2441
2595
  recovery: "resume_or_fresh"
2442
2596
  };
2443
2597
  model = {
2444
- detectedModelsVerifiedAs: "launchable",
2445
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2598
+ detectedModelsVerifiedAs: "suggestion_only",
2599
+ toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
2446
2600
  };
2447
2601
  supportsStdinNotification = false;
2448
2602
  mcpToolPrefix = "";
2449
2603
  busyDeliveryMode = "none";
2604
+ usesSlockCliForCommunication = true;
2450
2605
  sessionId = null;
2451
2606
  sessionAnnounced = false;
2452
2607
  spawn(ctx) {
2453
2608
  this.sessionId = ctx.config.sessionId || null;
2454
2609
  this.sessionAnnounced = false;
2455
- const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2456
- if (!existsSync4(geminiDir)) {
2457
- mkdirSync3(geminiDir, { recursive: true });
2458
- }
2459
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2460
- const mcpCommand = isTsSource ? "npx" : "node";
2461
- 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];
2462
- const settingsPath = path7.join(geminiDir, "settings.json");
2463
- writeFileSync5(settingsPath, JSON.stringify({
2464
- mcpServers: {
2465
- chat: {
2466
- command: mcpCommand,
2467
- args: mcpArgs
2468
- }
2469
- }
2470
- }), "utf8");
2610
+ this.writeGeminiSettings(ctx);
2471
2611
  const args = [
2472
2612
  "--output-format",
2473
2613
  "stream-json",
@@ -2541,28 +2681,64 @@ var GeminiDriver = class {
2541
2681
  return null;
2542
2682
  }
2543
2683
  buildSystemPrompt(config, _agentId) {
2544
- return buildMcpSystemPrompt(config, {
2684
+ return buildCliTransportSystemPrompt(config, {
2545
2685
  toolPrefix: "",
2546
2686
  extraCriticalRules: [
2547
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
2687
+ "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2688
+ ],
2689
+ postStartupNotes: [
2690
+ "**Gemini runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
2548
2691
  ],
2549
- postStartupNotes: [],
2550
2692
  includeStdinNotificationSection: false,
2551
2693
  messageNotificationStyle: "poll"
2552
2694
  });
2553
2695
  }
2696
+ writeGeminiSettings(ctx) {
2697
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2698
+ mkdirSync3(geminiDir, { recursive: true });
2699
+ const settingsPath = path7.join(geminiDir, "settings.json");
2700
+ writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
2701
+ }
2702
+ buildRuntimeActionsMcpSettings(ctx) {
2703
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2704
+ const command = isTsSource ? "npx" : "node";
2705
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2706
+ return {
2707
+ mcpServers: {
2708
+ chat: {
2709
+ command,
2710
+ args: [
2711
+ ...bridgeArgs,
2712
+ "--agent-id",
2713
+ ctx.agentId,
2714
+ "--server-url",
2715
+ ctx.config.serverUrl,
2716
+ "--auth-token",
2717
+ ctx.config.authToken || ctx.daemonApiKey,
2718
+ "--runtime",
2719
+ this.id,
2720
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2721
+ "--runtime-actions-only"
2722
+ ]
2723
+ }
2724
+ }
2725
+ };
2726
+ }
2554
2727
  };
2555
2728
 
2556
2729
  // src/drivers/kimi.ts
2557
2730
  import { randomUUID } from "crypto";
2558
2731
  import { spawn as spawn6 } from "child_process";
2559
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync6 } from "fs";
2560
- import os2 from "os";
2732
+ import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2733
+ import os3 from "os";
2561
2734
  import path8 from "path";
2562
2735
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2563
2736
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
2564
2737
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
2565
2738
  var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
2739
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
2740
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
2741
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
2566
2742
  function parseToolArguments(raw) {
2567
2743
  if (typeof raw !== "string") return raw;
2568
2744
  try {
@@ -2571,6 +2747,73 @@ function parseToolArguments(raw) {
2571
2747
  return raw;
2572
2748
  }
2573
2749
  }
2750
+ function readKimiConfigSource(home = os3.homedir(), env = process.env) {
2751
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2752
+ if (inlineConfig && inlineConfig.trim()) {
2753
+ return {
2754
+ raw: inlineConfig,
2755
+ explicitPath: null,
2756
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
2757
+ };
2758
+ }
2759
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
2760
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path8.join(home, ".kimi", "config.toml");
2761
+ try {
2762
+ return {
2763
+ raw: readFileSync3(configPath, "utf8"),
2764
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2765
+ sourcePath: configPath
2766
+ };
2767
+ } catch {
2768
+ return {
2769
+ raw: null,
2770
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2771
+ sourcePath: configPath
2772
+ };
2773
+ }
2774
+ }
2775
+ function buildKimiSpawnEnv(env = process.env) {
2776
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
2777
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2778
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
2779
+ return spawnEnv;
2780
+ }
2781
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
2782
+ return {
2783
+ ...process.env,
2784
+ ...ctx.config.envVars || {},
2785
+ ...overrideEnv || {}
2786
+ };
2787
+ }
2788
+ function buildKimiLaunchOptions(ctx, opts = {}) {
2789
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
2790
+ const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
2791
+ const args = [];
2792
+ let configFilePath = null;
2793
+ let configContent = null;
2794
+ if (source.explicitPath) {
2795
+ configFilePath = source.explicitPath;
2796
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
2797
+ configFilePath = path8.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
2798
+ configContent = source.raw;
2799
+ if (opts.writeGeneratedConfig !== false) {
2800
+ writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
2801
+ chmodSync(configFilePath, 384);
2802
+ }
2803
+ }
2804
+ if (configFilePath) {
2805
+ args.push("--config-file", configFilePath);
2806
+ }
2807
+ if (ctx.config.model && ctx.config.model !== "default") {
2808
+ args.push("--model", ctx.config.model);
2809
+ }
2810
+ return {
2811
+ args,
2812
+ env: buildKimiSpawnEnv(env),
2813
+ configFilePath,
2814
+ configContent
2815
+ };
2816
+ }
2574
2817
  var KimiDriver = class {
2575
2818
  id = "kimi";
2576
2819
  lifecycle = {
@@ -2587,7 +2830,25 @@ var KimiDriver = class {
2587
2830
  };
2588
2831
  model = {
2589
2832
  detectedModelsVerifiedAs: "launchable",
2590
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2833
+ toLaunchSpec: (modelId, ctx, opts) => {
2834
+ if (!ctx) return { args: ["--model", modelId] };
2835
+ const launchCtx = {
2836
+ ...ctx,
2837
+ config: {
2838
+ ...ctx.config,
2839
+ model: modelId
2840
+ }
2841
+ };
2842
+ const launch = buildKimiLaunchOptions(launchCtx, {
2843
+ home: opts?.home,
2844
+ writeGeneratedConfig: false
2845
+ });
2846
+ return {
2847
+ args: launch.args,
2848
+ env: launch.env,
2849
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
2850
+ };
2851
+ }
2591
2852
  };
2592
2853
  supportsStdinNotification = true;
2593
2854
  mcpToolPrefix = "";
@@ -2639,6 +2900,7 @@ var KimiDriver = class {
2639
2900
  }
2640
2901
  }
2641
2902
  }), "utf8");
2903
+ const launch = buildKimiLaunchOptions(ctx);
2642
2904
  const args = [
2643
2905
  "--wire",
2644
2906
  "--yolo",
@@ -2647,16 +2909,13 @@ var KimiDriver = class {
2647
2909
  "--mcp-config-file",
2648
2910
  mcpConfigPath,
2649
2911
  "--session",
2650
- this.sessionId
2912
+ this.sessionId,
2913
+ ...launch.args
2651
2914
  ];
2652
- if (ctx.config.model && ctx.config.model !== "default") {
2653
- args.push("--model", ctx.config.model);
2654
- }
2655
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
2656
2915
  const proc = spawn6("kimi", args, {
2657
2916
  cwd: ctx.workingDirectory,
2658
2917
  stdio: ["pipe", "pipe", "pipe"],
2659
- env: spawnEnv,
2918
+ env: launch.env,
2660
2919
  shell: process.platform === "win32"
2661
2920
  });
2662
2921
  proc.stdin?.write(JSON.stringify({
@@ -2773,14 +3032,9 @@ var KimiDriver = class {
2773
3032
  return detectKimiModels();
2774
3033
  }
2775
3034
  };
2776
- function detectKimiModels(home = os2.homedir()) {
2777
- const configPath = path8.join(home, ".kimi", "config.toml");
2778
- let raw;
2779
- try {
2780
- raw = readFileSync2(configPath, "utf8");
2781
- } catch {
2782
- return null;
2783
- }
3035
+ function detectKimiModels(home = os3.homedir(), opts = {}) {
3036
+ const raw = readKimiConfigSource(home, opts.env).raw;
3037
+ if (raw === null) return null;
2784
3038
  const models = [];
2785
3039
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
2786
3040
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -2800,9 +3054,9 @@ function detectKimiModels(home = os2.homedir()) {
2800
3054
  }
2801
3055
 
2802
3056
  // src/drivers/opencode.ts
2803
- import { spawn as spawn7 } from "child_process";
2804
- import { readFileSync as readFileSync3 } from "fs";
2805
- import os3 from "os";
3057
+ import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
3058
+ import { readFileSync as readFileSync4 } from "fs";
3059
+ import os4 from "os";
2806
3060
  import path9 from "path";
2807
3061
  var CHAT_MCP_SERVER_NAME = "chat";
2808
3062
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
@@ -2810,6 +3064,14 @@ var SLOCK_AGENT_NAME = "slock";
2810
3064
  var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2811
3065
  var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2812
3066
  var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
3067
+ var OPENCODE_PROVIDER_LABELS = {
3068
+ opencode: "OpenCode",
3069
+ "opencode-go": "OpenCode Go",
3070
+ openai: "OpenAI",
3071
+ openrouter: "OpenRouter",
3072
+ deepseek: "DeepSeek",
3073
+ fusecode: "FuseCode"
3074
+ };
2813
3075
  function buildChatBridgeCommand(ctx) {
2814
3076
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2815
3077
  return [
@@ -2842,10 +3104,10 @@ function parseUserOpenCodeConfig(ctx) {
2842
3104
  const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2843
3105
  return parseOpenCodeConfigContent(raw);
2844
3106
  }
2845
- function readLocalOpenCodeConfig(home = os3.homedir()) {
3107
+ function readLocalOpenCodeConfig(home = os4.homedir()) {
2846
3108
  const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2847
3109
  try {
2848
- return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
3110
+ return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
2849
3111
  } catch {
2850
3112
  }
2851
3113
  return {};
@@ -2891,7 +3153,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
2891
3153
  }
2892
3154
  };
2893
3155
  }
2894
- function buildOpenCodeConfig(ctx, home = os3.homedir()) {
3156
+ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
2895
3157
  const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2896
3158
  const userAgents = recordField(userConfig.agent);
2897
3159
  const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
@@ -2916,7 +3178,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2916
3178
  }
2917
3179
  };
2918
3180
  }
2919
- function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
3181
+ function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
2920
3182
  const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2921
3183
  const config = buildOpenCodeConfig(ctx, home);
2922
3184
  const env = {
@@ -2943,24 +3205,92 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2943
3205
  args.push("--", turnPrompt);
2944
3206
  return { args, env, config };
2945
3207
  }
2946
- function detectOpenCodeModels(home = os3.homedir()) {
2947
- const models = (RUNTIME_MODELS.opencode || []).map((model) => ({
2948
- ...model,
2949
- verified: "suggestion_only"
2950
- }));
2951
- const providers = recordField(readLocalOpenCodeConfig(home).provider);
2952
- for (const [providerId, providerConfig] of Object.entries(providers)) {
2953
- const providerModels = recordField(recordField(providerConfig).models);
2954
- for (const [modelId, modelConfig] of Object.entries(providerModels)) {
2955
- const fullId = `${providerId}/${modelId}`;
2956
- if (models.some((model2) => model2.id === fullId)) continue;
2957
- const model = recordField(modelConfig);
2958
- const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
2959
- models.push({ id: fullId, label: name, verified: "launchable" });
2960
- }
3208
+ function parseOpenCodeModelsOutput(output) {
3209
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
3210
+ const models = [];
3211
+ const seen = /* @__PURE__ */ new Set();
3212
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
3213
+ const line = rawLine.trim();
3214
+ if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
3215
+ if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
3216
+ if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
3217
+ if (seen.has(line)) continue;
3218
+ seen.add(line);
3219
+ models.push({
3220
+ id: line,
3221
+ label: formatOpenCodeModelLabel(line),
3222
+ verified: "launchable"
3223
+ });
2961
3224
  }
2962
3225
  return models.length > 0 ? { models } : null;
2963
3226
  }
3227
+ function formatOpenCodeModelLabel(modelId) {
3228
+ const separatorIndex = modelId.indexOf("/");
3229
+ if (separatorIndex <= 0 || separatorIndex === modelId.length - 1) return modelId;
3230
+ const providerId = modelId.slice(0, separatorIndex);
3231
+ const modelName = modelId.slice(separatorIndex + 1);
3232
+ const providerLabel = OPENCODE_PROVIDER_LABELS[providerId] || humanizeOpenCodeSegment(providerId);
3233
+ const modelParts = modelName.split("/");
3234
+ const modelLabel = humanizeOpenCodeSegment(modelParts[modelParts.length - 1] || modelName);
3235
+ if (modelParts.length === 1) return `${modelLabel} \xB7 ${providerLabel}`;
3236
+ const upstreamLabel = modelParts.slice(0, -1).map(humanizeOpenCodeSegment).join(" / ");
3237
+ return `${modelLabel} \xB7 ${upstreamLabel} via ${providerLabel}`;
3238
+ }
3239
+ function humanizeOpenCodeSegment(value) {
3240
+ return value.replace(/\[(\d+)m\]/gi, "-$1m").split(/[-_]/).filter(Boolean).map(formatOpenCodeLabelToken).join(" ");
3241
+ }
3242
+ function formatOpenCodeLabelToken(token) {
3243
+ const normalized = token.toLowerCase();
3244
+ const specialCases = {
3245
+ ai: "AI",
3246
+ api: "API",
3247
+ b: "B",
3248
+ chatgpt: "ChatGPT",
3249
+ claude: "Claude",
3250
+ codestral: "Codestral",
3251
+ deepseek: "DeepSeek",
3252
+ flash: "Flash",
3253
+ free: "Free",
3254
+ gemini: "Gemini",
3255
+ glm: "GLM",
3256
+ gpt: "GPT",
3257
+ hy3: "HY3",
3258
+ kimi: "Kimi",
3259
+ m: "M",
3260
+ minimax: "MiniMax",
3261
+ nano: "Nano",
3262
+ nemotron: "Nemotron",
3263
+ omni: "Omni",
3264
+ opus: "Opus",
3265
+ pro: "Pro",
3266
+ sonnet: "Sonnet",
3267
+ super: "Super"
3268
+ };
3269
+ if (specialCases[normalized]) return specialCases[normalized];
3270
+ if (/^v\d+(\.\d+)?$/.test(normalized)) return normalized.toUpperCase();
3271
+ if (/^\d+m$/i.test(token)) return token.toUpperCase();
3272
+ if (/^\d+[bk]$/i.test(token)) return token.toUpperCase();
3273
+ if (/^m\d+(\.\d+)?$/i.test(token)) return token.toUpperCase();
3274
+ if (/^\d/.test(token)) return token;
3275
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
3276
+ }
3277
+ function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3278
+ const commandResult = runCommand(home);
3279
+ if (commandResult.error || commandResult.status !== 0) return null;
3280
+ return parseOpenCodeModelsOutput(commandResult.stdout);
3281
+ }
3282
+ function runOpenCodeModelsCommand(home) {
3283
+ const result = spawnSync2("opencode", ["models"], {
3284
+ env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
3285
+ encoding: "utf8",
3286
+ timeout: 5e3
3287
+ });
3288
+ return {
3289
+ status: result.status,
3290
+ stdout: String(result.stdout || ""),
3291
+ error: result.error
3292
+ };
3293
+ }
2964
3294
  function isSystemFirstMessageTask(message) {
2965
3295
  return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2966
3296
  }
@@ -3236,9 +3566,39 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3236
3566
  }
3237
3567
 
3238
3568
  // src/agentProcessManager.ts
3239
- var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
3240
- var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 1;
3569
+ var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3570
+ var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3241
3571
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3572
+ var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
3573
+ var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
3574
+ var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
3575
+ ".md",
3576
+ ".txt",
3577
+ ".json",
3578
+ ".js",
3579
+ ".ts",
3580
+ ".jsx",
3581
+ ".tsx",
3582
+ ".yaml",
3583
+ ".yml",
3584
+ ".toml",
3585
+ ".log",
3586
+ ".csv",
3587
+ ".xml",
3588
+ ".html",
3589
+ ".css",
3590
+ ".sh",
3591
+ ".py"
3592
+ ]);
3593
+ var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
3594
+ ".apng": "image/apng",
3595
+ ".avif": "image/avif",
3596
+ ".gif": "image/gif",
3597
+ ".jpg": "image/jpeg",
3598
+ ".jpeg": "image/jpeg",
3599
+ ".png": "image/png",
3600
+ ".webp": "image/webp"
3601
+ };
3242
3602
  function readPositiveIntegerEnv(name, fallback) {
3243
3603
  const raw = process.env[name];
3244
3604
  if (!raw) return fallback;
@@ -3278,6 +3638,7 @@ function formatMessageTarget(message) {
3278
3638
  function getMessageShortId(messageId) {
3279
3639
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
3280
3640
  }
3641
+ var RESPONSE_TARGET_HINT = "Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.";
3281
3642
  function findSessionJsonl(root, predicate) {
3282
3643
  let visited = 0;
3283
3644
  const maxEntries = 1e4;
@@ -3286,7 +3647,7 @@ function findSessionJsonl(root, predicate) {
3286
3647
  if (depth < 0 || visited >= maxEntries) return null;
3287
3648
  let entries;
3288
3649
  try {
3289
- entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3650
+ entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3290
3651
  } catch {
3291
3652
  return null;
3292
3653
  }
@@ -3332,11 +3693,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3332
3693
  return null;
3333
3694
  }
3334
3695
  }
3335
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3696
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3336
3697
  const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3337
3698
  if (directPath) {
3338
3699
  try {
3339
- if (statSync(directPath).isFile()) {
3700
+ if (statSync2(directPath).isFile()) {
3340
3701
  return { label: sessionId, path: directPath, runtime, reachable: true };
3341
3702
  }
3342
3703
  } catch {
@@ -3432,6 +3793,16 @@ function formatRuntimeProfileControlPrompt(messages) {
3432
3793
  return null;
3433
3794
  }
3434
3795
  const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
3796
+ const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
3797
+ if (!hasMigration) {
3798
+ return [
3799
+ "Runtime Profile daemon release notice.",
3800
+ "",
3801
+ "Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
3802
+ "",
3803
+ body
3804
+ ].join("\n");
3805
+ }
3435
3806
  return [
3436
3807
  "Runtime Profile control notice.",
3437
3808
  "",
@@ -3937,6 +4308,15 @@ function classifyTerminalFailure(ap) {
3937
4308
  }
3938
4309
  return null;
3939
4310
  }
4311
+ function hasDirectStdinRecoveryEvidence(ap) {
4312
+ const candidates = [
4313
+ ap.lastRuntimeError,
4314
+ ...ap.recentStderr
4315
+ ].filter((value) => !!value);
4316
+ return candidates.some(
4317
+ (text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
4318
+ );
4319
+ }
3940
4320
  function isMissingResumeSession(ap) {
3941
4321
  if (!ap.sessionId) return false;
3942
4322
  const candidates = [
@@ -3953,6 +4333,66 @@ function isMissingResumeSession(ap) {
3953
4333
  }
3954
4334
  return false;
3955
4335
  }
4336
+ function classifyActivityDetailForTrace(detail) {
4337
+ if (!detail) return void 0;
4338
+ if (detail === "Message received") return "message_received";
4339
+ if (detail === "Starting\u2026") return "starting";
4340
+ if (detail === "Running command\u2026") return "running_command";
4341
+ if (detail === "Checking messages\u2026") return "checking_messages";
4342
+ if (detail === "Compacting context") return "compacting_context";
4343
+ if (detail === "Context compaction finished") return "compaction_finished";
4344
+ if (detail === "Context compaction still running; no finish event observed") return "compaction_stale";
4345
+ if (detail === "Idle" || detail === "Process idle") return "idle";
4346
+ if (detail.startsWith("Restarting stalled ") && detail.endsWith(" runtime for queued message")) return "stalled_recovery";
4347
+ if (detail.startsWith("Runtime stalled: no runtime events for ")) return "runtime_stalled";
4348
+ return "other";
4349
+ }
4350
+ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
4351
+ const context = [];
4352
+ if (ap.lastActivityDetail) {
4353
+ context.push(`after ${ap.lastActivityDetail}`);
4354
+ }
4355
+ if (ap.driver.busyDeliveryMode === "gated") {
4356
+ context.push(`phase=${ap.gatedSteering.phase}`);
4357
+ }
4358
+ if (ap.gatedSteering.outstandingToolUses > 0) {
4359
+ context.push(`tools=${ap.gatedSteering.outstandingToolUses}`);
4360
+ }
4361
+ if (ap.gatedSteering.compacting) {
4362
+ context.push("compacting");
4363
+ }
4364
+ if (ap.inbox.length > 0) {
4365
+ context.push(`queued=${ap.inbox.length}`);
4366
+ }
4367
+ const detail = [
4368
+ `Runtime stalled: no runtime events for ${staleForMinutes}m`,
4369
+ context.length > 0 ? ` (${context.join(", ")})` : ""
4370
+ ].join("");
4371
+ return {
4372
+ detail,
4373
+ traceAttrs: {
4374
+ ageMs: staleForMs,
4375
+ staleForMinutes,
4376
+ lastActivity: ap.lastActivity,
4377
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
4378
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
4379
+ runtime: ap.config.runtime,
4380
+ model: ap.config.model,
4381
+ launchId: ap.launchId || void 0,
4382
+ sessionIdPresent: Boolean(ap.sessionId),
4383
+ inboxCount: ap.inbox.length,
4384
+ pendingNotificationCount: ap.pendingNotificationCount,
4385
+ processPidPresent: typeof ap.process.pid === "number",
4386
+ busyDeliveryMode: ap.driver.busyDeliveryMode,
4387
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
4388
+ gatedPhase: ap.driver.busyDeliveryMode === "gated" ? ap.gatedSteering.phase : void 0,
4389
+ outstandingToolUses: ap.gatedSteering.outstandingToolUses,
4390
+ compacting: ap.gatedSteering.compacting,
4391
+ recentStderrCount: ap.recentStderr.length,
4392
+ recentStdoutCount: ap.recentStdout.length
4393
+ }
4394
+ };
4395
+ }
3956
4396
  function getMessageDeliveryText(driver) {
3957
4397
  return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
3958
4398
  }
@@ -3978,7 +4418,6 @@ var AgentProcessManager = class _AgentProcessManager {
3978
4418
  // Prevent concurrent starts of same agent
3979
4419
  queuedAgentStarts = /* @__PURE__ */ new Map();
3980
4420
  agentStartQueue = [];
3981
- activeAgentStartPermits = /* @__PURE__ */ new Set();
3982
4421
  activeAgentStartCount = 0;
3983
4422
  agentStartPumpTimer = null;
3984
4423
  lastAgentStartAt = 0;
@@ -3998,6 +4437,7 @@ var AgentProcessManager = class _AgentProcessManager {
3998
4437
  driverResolver;
3999
4438
  defaultAgentEnvVarsProvider;
4000
4439
  tracer;
4440
+ deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
4001
4441
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
4002
4442
  this.chatBridgePath = chatBridgePath;
4003
4443
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -4005,7 +4445,7 @@ var AgentProcessManager = class _AgentProcessManager {
4005
4445
  this.daemonApiKey = daemonApiKey;
4006
4446
  this.serverUrl = opts.serverUrl;
4007
4447
  this.dataDir = opts.dataDir || DATA_DIR;
4008
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
4448
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
4009
4449
  this.driverResolver = opts.driverResolver || getDriver;
4010
4450
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
4011
4451
  this.tracer = opts.tracer ?? noopTracer;
@@ -4022,16 +4462,75 @@ var AgentProcessManager = class _AgentProcessManager {
4022
4462
  )
4023
4463
  );
4024
4464
  }
4465
+ setTracer(tracer) {
4466
+ this.tracer = tracer;
4467
+ }
4468
+ recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
4469
+ const span = this.tracer.startSpan(name, {
4470
+ parent: parseTraceparent(parentTraceparent),
4471
+ surface: "daemon",
4472
+ kind: "internal",
4473
+ attrs
4474
+ });
4475
+ span.end(status);
4476
+ }
4477
+ startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4478
+ return {
4479
+ agentId,
4480
+ launchId,
4481
+ runtime: config.runtime,
4482
+ model: config.model,
4483
+ session_id_present: Boolean(config.sessionId),
4484
+ launch_id_present: Boolean(launchId),
4485
+ wake_message_present: Boolean(wakeMessage),
4486
+ unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
4487
+ resume_prompt_present: Boolean(resumePrompt),
4488
+ queue_depth: this.agentStartQueue.length,
4489
+ active_starts: this.activeAgentStartCount,
4490
+ max_concurrent_starts: this.maxConcurrentAgentStarts,
4491
+ min_start_interval_ms: this.agentStartIntervalMs
4492
+ };
4493
+ }
4494
+ getDeliveryTraceContext(message) {
4495
+ return this.deliveryTraceContexts.get(message) ?? {};
4496
+ }
4497
+ deliveryTraceAttrs(agentId, message, attrs = {}) {
4498
+ const context = this.getDeliveryTraceContext(message);
4499
+ const deliveryCorrelationId = context.deliveryId ?? message.message_id;
4500
+ return {
4501
+ agentId,
4502
+ deliveryId: context.deliveryId,
4503
+ delivery_correlation_id: deliveryCorrelationId,
4504
+ channel_type: message.channel_type,
4505
+ sender_type: message.sender_type,
4506
+ messageId: message.message_id,
4507
+ message_id_present: Boolean(message.message_id),
4508
+ ...attrs
4509
+ };
4510
+ }
4025
4511
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4512
+ this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4026
4513
  if (this.agents.has(agentId)) {
4514
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4515
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4516
+ reason: "already_running"
4517
+ });
4027
4518
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4028
4519
  return;
4029
4520
  }
4030
4521
  if (this.agentsStarting.has(agentId)) {
4522
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4523
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4524
+ reason: "already_starting"
4525
+ });
4031
4526
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4032
4527
  return;
4033
4528
  }
4034
4529
  if (this.queuedAgentStarts.has(agentId)) {
4530
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4531
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4532
+ reason: "already_queued"
4533
+ });
4035
4534
  logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
4036
4535
  return;
4037
4536
  }
@@ -4048,6 +4547,7 @@ var AgentProcessManager = class _AgentProcessManager {
4048
4547
  };
4049
4548
  this.agentStartQueue.push(item);
4050
4549
  this.queuedAgentStarts.set(agentId, item);
4550
+ this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4051
4551
  logger.info(
4052
4552
  `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
4053
4553
  );
@@ -4063,6 +4563,10 @@ var AgentProcessManager = class _AgentProcessManager {
4063
4563
  const elapsed = Date.now() - this.lastAgentStartAt;
4064
4564
  const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
4065
4565
  if (waitMs > 0) {
4566
+ this.recordDaemonTrace("daemon.agent.start.rate_limited", {
4567
+ ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
4568
+ wait_ms: waitMs
4569
+ });
4066
4570
  this.agentStartPumpTimer = setTimeout(() => {
4067
4571
  this.agentStartPumpTimer = null;
4068
4572
  this.pumpAgentStartQueue();
@@ -4072,23 +4576,31 @@ var AgentProcessManager = class _AgentProcessManager {
4072
4576
  const item = this.agentStartQueue.shift();
4073
4577
  if (!item) return;
4074
4578
  if (this.queuedAgentStarts.get(item.agentId) !== item) {
4579
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4580
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4581
+ reason: "stale_queue_item"
4582
+ });
4075
4583
  this.pumpAgentStartQueue();
4076
4584
  return;
4077
4585
  }
4078
4586
  this.queuedAgentStarts.delete(item.agentId);
4079
4587
  if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
4588
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4589
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4590
+ reason: "already_running_or_starting"
4591
+ });
4080
4592
  logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
4081
4593
  item.resolve();
4082
4594
  this.pumpAgentStartQueue();
4083
4595
  return;
4084
4596
  }
4085
4597
  this.activeAgentStartCount++;
4086
- this.activeAgentStartPermits.add(item.agentId);
4087
4598
  this.lastAgentStartAt = Date.now();
4088
4599
  this.lastAgentStartAgentId = item.agentId;
4089
4600
  logger.info(
4090
4601
  `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
4091
4602
  );
4603
+ this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
4092
4604
  this.startAgentNow(
4093
4605
  item.agentId,
4094
4606
  item.config,
@@ -4096,19 +4608,28 @@ var AgentProcessManager = class _AgentProcessManager {
4096
4608
  item.unreadSummary,
4097
4609
  item.resumePrompt,
4098
4610
  item.launchId
4099
- ).then(item.resolve, (err) => {
4100
- this.releaseAgentStartPermit(item.agentId, "start failed");
4611
+ ).then(() => {
4612
+ this.releaseAgentStartSlot(item.agentId, "spawn attempted");
4613
+ item.resolve();
4614
+ }, (err) => {
4615
+ this.releaseAgentStartSlot(item.agentId, "start failed");
4101
4616
  item.reject(err);
4102
4617
  });
4103
4618
  }
4104
- releaseAgentStartPermit(agentId, reason) {
4105
- if (!this.activeAgentStartPermits.delete(agentId)) return false;
4619
+ releaseAgentStartSlot(agentId, reason) {
4620
+ if (this.activeAgentStartCount <= 0) return;
4106
4621
  this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
4622
+ this.recordDaemonTrace("daemon.agent.start.slot_released", {
4623
+ agentId,
4624
+ reason,
4625
+ active_starts: this.activeAgentStartCount,
4626
+ queue_depth: this.agentStartQueue.length,
4627
+ max_concurrent_starts: this.maxConcurrentAgentStarts
4628
+ });
4107
4629
  logger.info(
4108
- `[Agent ${agentId}] Start permit released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4630
+ `[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4109
4631
  );
4110
4632
  this.pumpAgentStartQueue();
4111
- return true;
4112
4633
  }
4113
4634
  cancelQueuedAgentStart(agentId, reason) {
4114
4635
  const item = this.queuedAgentStarts.get(agentId);
@@ -4120,6 +4641,10 @@ var AgentProcessManager = class _AgentProcessManager {
4120
4641
  clearTimeout(this.agentStartPumpTimer);
4121
4642
  this.agentStartPumpTimer = null;
4122
4643
  }
4644
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4645
+ ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4646
+ reason
4647
+ }, "cancelled");
4123
4648
  logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
4124
4649
  item.resolve();
4125
4650
  return true;
@@ -4127,6 +4652,10 @@ var AgentProcessManager = class _AgentProcessManager {
4127
4652
  cancelAllQueuedAgentStarts(reason) {
4128
4653
  for (const item of this.agentStartQueue) {
4129
4654
  if (this.queuedAgentStarts.get(item.agentId) === item) {
4655
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4656
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4657
+ reason
4658
+ }, "cancelled");
4130
4659
  logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
4131
4660
  item.resolve();
4132
4661
  }
@@ -4141,14 +4670,23 @@ var AgentProcessManager = class _AgentProcessManager {
4141
4670
  }
4142
4671
  async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4143
4672
  if (this.agents.has(agentId)) {
4673
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4674
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4675
+ reason: "already_running"
4676
+ });
4144
4677
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4145
4678
  return;
4146
4679
  }
4147
4680
  if (this.agentsStarting.has(agentId)) {
4681
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4682
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4683
+ reason: "already_starting"
4684
+ });
4148
4685
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4149
4686
  return;
4150
4687
  }
4151
4688
  this.agentsStarting.add(agentId);
4689
+ this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4152
4690
  try {
4153
4691
  const driver = this.driverResolver(config.runtime || "claude");
4154
4692
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4208,6 +4746,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
4208
4746
  prompt += `
4209
4747
 
4210
4748
  Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
4749
+ ${RESPONSE_TARGET_HINT}
4211
4750
 
4212
4751
  IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
4213
4752
  prompt += getBusyDeliveryNote(driver);
@@ -4240,8 +4779,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4240
4779
  });
4241
4780
  this.sendAgentStatus(agentId, "active", launchId || null);
4242
4781
  this.broadcastActivity(agentId, "online", "Process idle");
4782
+ this.recordDaemonTrace("daemon.agent.spawn.deferred", {
4783
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4784
+ pending_messages_count: pendingMessages.length,
4785
+ reason: "defer_until_concrete_message"
4786
+ });
4243
4787
  logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
4244
- this.releaseAgentStartPermit(agentId, "spawn deferred");
4245
4788
  for (const message of pendingMessages) {
4246
4789
  this.deliverMessage(agentId, message);
4247
4790
  }
@@ -4258,6 +4801,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4258
4801
  daemonApiKey: this.daemonApiKey,
4259
4802
  launchId: launchId || null
4260
4803
  });
4804
+ this.recordDaemonTrace("daemon.agent.spawn.created", {
4805
+ ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
4806
+ detached: false,
4807
+ new_session: false,
4808
+ process_pid_present: typeof proc.pid === "number"
4809
+ });
4261
4810
  const agentProcess = {
4262
4811
  process: proc,
4263
4812
  driver,
@@ -4292,7 +4841,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4292
4841
  };
4293
4842
  this.startingInboxes.delete(agentId);
4294
4843
  this.agents.set(agentId, agentProcess);
4295
- this.startRuntimeTrace(agentId, agentProcess, "spawn");
4844
+ this.idleAgentConfigs.set(agentId, {
4845
+ config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
4846
+ sessionId: effectiveConfig.sessionId || null,
4847
+ launchId: launchId || null
4848
+ });
4849
+ this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
4296
4850
  this.agentsStarting.delete(agentId);
4297
4851
  if (config.runtimeProfileControl) {
4298
4852
  this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
@@ -4331,6 +4885,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4331
4885
  proc.on("error", (err) => {
4332
4886
  const current = this.agents.get(agentId);
4333
4887
  if (current) current.spawnError = err.message;
4888
+ this.recordDaemonTrace("daemon.agent.process.error", {
4889
+ agentId,
4890
+ launchId: current?.launchId || void 0,
4891
+ runtime: config.runtime,
4892
+ model: config.model,
4893
+ error_class: err.name || typeof err
4894
+ }, "error");
4334
4895
  logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
4335
4896
  });
4336
4897
  proc.on("exit", (code, signal) => {
@@ -4339,13 +4900,24 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4339
4900
  current.exitCode = code;
4340
4901
  current.exitSignal = signal;
4341
4902
  }
4903
+ this.recordDaemonTrace("daemon.agent.process.exited", {
4904
+ agentId,
4905
+ launchId: current?.launchId || void 0,
4906
+ runtime: config.runtime,
4907
+ model: config.model,
4908
+ exit_code: code,
4909
+ exit_signal: signal,
4910
+ clean_exit: code === 0,
4911
+ runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4912
+ inbox_count: current?.inbox.length ?? 0,
4913
+ pending_notification_count: current?.pendingNotificationCount ?? 0
4914
+ });
4342
4915
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4343
4916
  });
4344
4917
  proc.on("close", (code, signal) => {
4345
4918
  if (this.agents.has(agentId)) {
4346
4919
  const ap = this.agents.get(agentId);
4347
4920
  if (ap.process !== proc) return;
4348
- this.releaseAgentStartPermit(agentId, "process closed");
4349
4921
  if (ap.notificationTimer) {
4350
4922
  clearTimeout(ap.notificationTimer);
4351
4923
  }
@@ -4403,7 +4975,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4403
4975
  }
4404
4976
  if (processEndedCleanly) {
4405
4977
  let queuedWakeMessage;
4406
- if (!ap.driver.supportsStdinNotification) {
4978
+ if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
4407
4979
  while (ap.inbox.length > 0) {
4408
4980
  const candidate = ap.inbox.shift();
4409
4981
  if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
@@ -4509,6 +5081,66 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4509
5081
  }
4510
5082
  return leftKeys.every((key) => left?.[key] === right?.[key]);
4511
5083
  }
5084
+ enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
5085
+ ap.inbox.push(message);
5086
+ if (ap.driver.supportsStdinNotification && ap.sessionId) {
5087
+ ap.pendingNotificationCount++;
5088
+ if (ap.driver.busyDeliveryMode === "gated") {
5089
+ this.recordGatedSteeringEvent(agentId, ap, "buffer", {
5090
+ reason: "runtime_profile",
5091
+ kind,
5092
+ pendingMessages: ap.inbox.length
5093
+ });
5094
+ } else if (!ap.notificationTimer) {
5095
+ ap.notificationTimer = setTimeout(() => {
5096
+ this.sendStdinNotification(agentId);
5097
+ }, 3e3);
5098
+ }
5099
+ }
5100
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
5101
+ agentId,
5102
+ kind,
5103
+ key_present: Boolean(key),
5104
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5105
+ runtime: ap.config.runtime,
5106
+ session_id_present: Boolean(ap.sessionId),
5107
+ launchId: ap.launchId || void 0,
5108
+ inbox_count: ap.inbox.length,
5109
+ pending_notification_count: ap.pendingNotificationCount,
5110
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
5111
+ supports_stdin_notification: ap.driver.supportsStdinNotification
5112
+ });
5113
+ logger.info(
5114
+ `[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
5115
+ );
5116
+ }
5117
+ queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
5118
+ const pending = this.startingInboxes.get(agentId) || [];
5119
+ pending.push(message);
5120
+ this.startingInboxes.set(agentId, pending);
5121
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5122
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
5123
+ agentId,
5124
+ kind,
5125
+ key_present: Boolean(key),
5126
+ outcome: "queued_during_start",
5127
+ startup_pending: true,
5128
+ starting_inbox_count: pending.length,
5129
+ launchId: queuedStart?.launchId
5130
+ });
5131
+ logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
5132
+ }
5133
+ splitRuntimeProfileControlBatch(messages) {
5134
+ const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
5135
+ if (controlMessages.length === 0 || controlMessages.length === messages.length) {
5136
+ return { nextMessages: messages, deferredMessages: [] };
5137
+ }
5138
+ const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
5139
+ return { nextMessages: controlMessages, deferredMessages };
5140
+ }
5141
+ containsOrdinaryInboxMessage(messages) {
5142
+ return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
5143
+ }
4512
5144
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
4513
5145
  this.cancelQueuedAgentStart(agentId, "stop requested");
4514
5146
  this.idleAgentConfigs.delete(agentId);
@@ -4519,7 +5151,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4519
5151
  }
4520
5152
  return;
4521
5153
  }
4522
- this.releaseAgentStartPermit(agentId, "stop requested");
4523
5154
  if (ap.notificationTimer) {
4524
5155
  clearTimeout(ap.notificationTimer);
4525
5156
  }
@@ -4558,51 +5189,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4558
5189
  });
4559
5190
  }
4560
5191
  }
4561
- deliverMessage(agentId, message) {
5192
+ deliverMessage(agentId, message, traceContext = {}) {
5193
+ if (traceContext.deliveryId) {
5194
+ this.deliveryTraceContexts.set(message, traceContext);
5195
+ }
4562
5196
  const ap = this.agents.get(agentId);
4563
5197
  if (!ap) {
4564
5198
  if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5199
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4565
5200
  const pending = this.startingInboxes.get(agentId) || [];
4566
5201
  pending.push(message);
4567
5202
  this.startingInboxes.set(agentId, pending);
4568
- return;
5203
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5204
+ outcome: "queued_during_start",
5205
+ accepted: true,
5206
+ process_present: false,
5207
+ startup_pending: true,
5208
+ starting_inbox_count: pending.length,
5209
+ launchId: queuedStart?.launchId
5210
+ }));
5211
+ return true;
4569
5212
  }
4570
5213
  const cached = this.idleAgentConfigs.get(agentId);
4571
5214
  if (cached) {
4572
5215
  const driver = this.driverResolver(cached.config.runtime || "claude");
4573
5216
  if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4574
- return;
5217
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5218
+ outcome: "deferred_wake_message",
5219
+ accepted: true,
5220
+ process_present: false,
5221
+ cached_idle_config_present: true,
5222
+ runtime: cached.config.runtime,
5223
+ session_id_present: Boolean(cached.sessionId),
5224
+ launchId: cached.launchId || void 0
5225
+ }));
5226
+ return true;
4575
5227
  }
4576
5228
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
4577
5229
  this.idleAgentConfigs.delete(agentId);
5230
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5231
+ outcome: "auto_restart_from_idle",
5232
+ accepted: true,
5233
+ process_present: false,
5234
+ cached_idle_config_present: true,
5235
+ runtime: cached.config.runtime,
5236
+ session_id_present: Boolean(cached.sessionId),
5237
+ launchId: cached.launchId || void 0
5238
+ }));
4578
5239
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
4579
5240
  logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
4580
5241
  });
5242
+ return true;
4581
5243
  }
4582
- return;
5244
+ logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
5245
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5246
+ outcome: "rejected_no_process",
5247
+ accepted: false,
5248
+ process_present: false,
5249
+ cached_idle_config_present: false
5250
+ }), "error");
5251
+ this.sendAgentStatus(agentId, "inactive", null);
5252
+ this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
5253
+ return false;
4583
5254
  }
4584
5255
  if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4585
- return;
5256
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5257
+ outcome: "deferred_wake_message",
5258
+ accepted: true,
5259
+ process_present: true,
5260
+ runtime: ap.config.runtime,
5261
+ session_id_present: Boolean(ap.sessionId),
5262
+ launchId: ap.launchId || void 0,
5263
+ is_idle: ap.isIdle,
5264
+ inbox_count: ap.inbox.length
5265
+ }));
5266
+ return true;
4586
5267
  }
4587
5268
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
4588
5269
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4589
5270
  nextMessages.push(message);
4590
5271
  ap.isIdle = false;
4591
- this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
5272
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
4592
5273
  this.broadcastActivity(agentId, "working", "Message received");
4593
- this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
4594
- return;
5274
+ const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
5275
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5276
+ outcome: "stdin_idle_delivery",
5277
+ accepted: true,
5278
+ process_present: true,
5279
+ runtime: ap.config.runtime,
5280
+ session_id_present: true,
5281
+ launchId: ap.launchId || void 0,
5282
+ stdin_delivery_accepted: stdinAccepted,
5283
+ delivered_messages_count: nextMessages.length
5284
+ }));
5285
+ return true;
4595
5286
  }
4596
5287
  ap.inbox.push(message);
4597
- if (!ap.driver.supportsStdinNotification) return;
4598
- if (!ap.sessionId) return;
5288
+ if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
5289
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5290
+ outcome: "queued_stalled_recovery",
5291
+ accepted: true,
5292
+ process_present: true,
5293
+ runtime: ap.config.runtime,
5294
+ session_id_present: Boolean(ap.sessionId),
5295
+ launchId: ap.launchId || void 0,
5296
+ inbox_count: ap.inbox.length
5297
+ }));
5298
+ return true;
5299
+ }
5300
+ if (!ap.driver.supportsStdinNotification) {
5301
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5302
+ outcome: "queued_busy_non_stdin",
5303
+ accepted: true,
5304
+ process_present: true,
5305
+ runtime: ap.config.runtime,
5306
+ session_id_present: Boolean(ap.sessionId),
5307
+ launchId: ap.launchId || void 0,
5308
+ inbox_count: ap.inbox.length
5309
+ }));
5310
+ return true;
5311
+ }
5312
+ if (!ap.sessionId) {
5313
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5314
+ outcome: "queued_before_session",
5315
+ accepted: true,
5316
+ process_present: true,
5317
+ runtime: ap.config.runtime,
5318
+ session_id_present: false,
5319
+ launchId: ap.launchId || void 0,
5320
+ inbox_count: ap.inbox.length
5321
+ }));
5322
+ return true;
5323
+ }
4599
5324
  if (ap.driver.busyDeliveryMode === "gated") {
4600
5325
  ap.pendingNotificationCount++;
4601
5326
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4602
5327
  reason: "busy_message",
4603
5328
  pendingMessages: ap.inbox.length
4604
5329
  });
4605
- return;
5330
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5331
+ outcome: "queued_busy_gated",
5332
+ accepted: true,
5333
+ process_present: true,
5334
+ runtime: ap.config.runtime,
5335
+ session_id_present: true,
5336
+ launchId: ap.launchId || void 0,
5337
+ inbox_count: ap.inbox.length,
5338
+ pending_notification_count: ap.pendingNotificationCount
5339
+ }));
5340
+ return true;
4606
5341
  }
4607
5342
  ap.pendingNotificationCount++;
4608
5343
  if (!ap.notificationTimer) {
@@ -4610,6 +5345,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4610
5345
  this.sendStdinNotification(agentId);
4611
5346
  }, 3e3);
4612
5347
  }
5348
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5349
+ outcome: "queued_busy_notification",
5350
+ accepted: true,
5351
+ process_present: true,
5352
+ runtime: ap.config.runtime,
5353
+ session_id_present: true,
5354
+ inbox_count: ap.inbox.length,
5355
+ pending_notification_count: ap.pendingNotificationCount,
5356
+ notification_timer_present: Boolean(ap.notificationTimer)
5357
+ }));
5358
+ return true;
4613
5359
  }
4614
5360
  async resetWorkspace(agentId) {
4615
5361
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4643,6 +5389,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4643
5389
  getIdleAgentSessionIds() {
4644
5390
  const result = [];
4645
5391
  for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
5392
+ if (this.agents.has(agentId)) continue;
4646
5393
  if (sessionId) result.push({ agentId, sessionId, launchId });
4647
5394
  }
4648
5395
  return result;
@@ -4694,7 +5441,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4694
5441
  }
4695
5442
  return reports;
4696
5443
  }
4697
- deliverRuntimeProfileNotification(agentId, key, kind, content) {
5444
+ deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
5445
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5446
+ parent: parseTraceparent(traceparent),
5447
+ surface: "daemon",
5448
+ kind: "consumer",
5449
+ attrs: {
5450
+ agentId,
5451
+ control_kind: kind,
5452
+ key_present: Boolean(key)
5453
+ }
5454
+ });
4698
5455
  const now = (/* @__PURE__ */ new Date()).toISOString();
4699
5456
  const message = {
4700
5457
  channel_id: "system",
@@ -4705,18 +5462,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4705
5462
  sender_type: "system",
4706
5463
  content,
4707
5464
  timestamp: now,
4708
- message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
5465
+ message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
5466
+ traceparent: formatTraceparent(span.context)
4709
5467
  };
4710
5468
  const ap = this.agents.get(agentId);
5469
+ if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
5470
+ this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
5471
+ span.end("ok", {
5472
+ attrs: {
5473
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5474
+ runtime: ap.config.runtime,
5475
+ launchId: ap.launchId || void 0,
5476
+ session_id_present: Boolean(ap.sessionId),
5477
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5478
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5479
+ }
5480
+ });
5481
+ return true;
5482
+ }
4711
5483
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
4712
5484
  ap.isIdle = false;
4713
5485
  this.startRuntimeTrace(agentId, ap, "runtime-profile");
4714
- this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
4715
- return;
5486
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5487
+ span.end(written ? "ok" : "error", {
5488
+ attrs: {
5489
+ outcome: written ? "stdin_idle" : "stdin_failed",
5490
+ runtime: ap.config.runtime,
5491
+ launchId: ap.launchId || void 0,
5492
+ session_id_present: true,
5493
+ supports_stdin_notification: true,
5494
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5495
+ }
5496
+ });
5497
+ return written;
4716
5498
  }
4717
5499
  if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
4718
- this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
4719
- return;
5500
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
5501
+ span.end(written ? "ok" : "error", {
5502
+ attrs: {
5503
+ outcome: written ? "stdin_busy" : "stdin_failed",
5504
+ runtime: ap.config.runtime,
5505
+ launchId: ap.launchId || void 0,
5506
+ session_id_present: true,
5507
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5508
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5509
+ }
5510
+ });
5511
+ return written;
5512
+ }
5513
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5514
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5515
+ this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
5516
+ span.end("ok", {
5517
+ attrs: {
5518
+ outcome: "queued_during_start",
5519
+ startup_pending: true,
5520
+ launchId: queuedStart?.launchId
5521
+ }
5522
+ });
5523
+ return true;
4720
5524
  }
4721
5525
  const cached = this.idleAgentConfigs.get(agentId);
4722
5526
  if (cached) {
@@ -4726,9 +5530,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4726
5530
  logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
4727
5531
  this.idleAgentConfigs.set(agentId, cached);
4728
5532
  });
4729
- return;
5533
+ span.end("ok", {
5534
+ attrs: {
5535
+ outcome: "restart_queued",
5536
+ runtime: cached.config.runtime,
5537
+ launchId: cached.launchId || void 0,
5538
+ session_id_present: Boolean(cached.sessionId)
5539
+ }
5540
+ });
5541
+ return true;
4730
5542
  }
4731
5543
  logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
5544
+ span.end("ok", { attrs: { outcome: "no_path" } });
5545
+ return false;
4732
5546
  }
4733
5547
  ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
4734
5548
  for (const message of messages) {
@@ -4741,19 +5555,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4741
5555
  type: "agent:runtime_profile:migration:ack",
4742
5556
  agentId,
4743
5557
  migrationKey: notification.key,
4744
- launchId: launchId || void 0
5558
+ launchId: launchId || void 0,
5559
+ traceparent: message.traceparent
4745
5560
  });
4746
5561
  } else {
4747
5562
  this.sendToServer({
4748
5563
  type: "agent:runtime_profile:daemon_release_notice:ack",
4749
5564
  agentId,
4750
5565
  noticeKey: notification.key,
4751
- launchId: launchId || void 0
5566
+ launchId: launchId || void 0,
5567
+ traceparent: message.traceparent
4752
5568
  });
4753
5569
  }
4754
5570
  }
4755
5571
  }
4756
5572
  ackInjectedRuntimeProfileControl(agentId, control, launchId) {
5573
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5574
+ surface: "daemon",
5575
+ kind: "internal",
5576
+ attrs: {
5577
+ agentId,
5578
+ control_kind: control.kind,
5579
+ key_present: Boolean(control.key),
5580
+ launchId: launchId || void 0,
5581
+ source: "agent_config"
5582
+ }
5583
+ });
4757
5584
  const title = runtimeProfileNotificationTitle(control.kind);
4758
5585
  this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
4759
5586
  if (control.kind === "migration") {
@@ -4761,24 +5588,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4761
5588
  type: "agent:runtime_profile:migration:ack",
4762
5589
  agentId,
4763
5590
  migrationKey: control.key,
4764
- launchId: launchId || void 0
5591
+ launchId: launchId || void 0,
5592
+ traceparent: formatTraceparent(span.context)
4765
5593
  });
4766
5594
  } else {
4767
5595
  this.sendToServer({
4768
5596
  type: "agent:runtime_profile:daemon_release_notice:ack",
4769
5597
  agentId,
4770
5598
  noticeKey: control.key,
4771
- launchId: launchId || void 0
5599
+ launchId: launchId || void 0,
5600
+ traceparent: formatTraceparent(span.context)
4772
5601
  });
4773
5602
  }
5603
+ span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
4774
5604
  }
4775
5605
  sendRuntimeProfileWireReport(report) {
5606
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
5607
+ surface: "daemon",
5608
+ kind: "producer",
5609
+ attrs: {
5610
+ agentId: report.agentId,
5611
+ launchId: report.launchId || void 0,
5612
+ runtime: report.facts.runtime,
5613
+ model_present: Boolean(report.facts.model),
5614
+ session_ref_present: Boolean(report.facts.sessionRef),
5615
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
5616
+ }
5617
+ });
4776
5618
  this.sendToServer({
4777
5619
  type: "agent:runtime_profile",
4778
5620
  agentId: report.agentId,
4779
5621
  facts: report.facts,
4780
- launchId: report.launchId || void 0
5622
+ launchId: report.launchId || void 0,
5623
+ traceparent: formatTraceparent(span.context)
4781
5624
  });
5625
+ span.end("ok");
4782
5626
  }
4783
5627
  sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4784
5628
  this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
@@ -4821,32 +5665,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4821
5665
  }
4822
5666
  const info = await stat2(resolved);
4823
5667
  if (info.isDirectory()) throw new Error("Cannot read a directory");
4824
- const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
4825
- ".md",
4826
- ".txt",
4827
- ".json",
4828
- ".js",
4829
- ".ts",
4830
- ".jsx",
4831
- ".tsx",
4832
- ".yaml",
4833
- ".yml",
4834
- ".toml",
4835
- ".log",
4836
- ".csv",
4837
- ".xml",
4838
- ".html",
4839
- ".css",
4840
- ".sh",
4841
- ".py"
4842
- ]);
4843
5668
  const ext = path11.extname(resolved).toLowerCase();
4844
- if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4845
- return { content: null, binary: true };
5669
+ if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5670
+ if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5671
+ const content = await readFile(resolved, "utf-8");
5672
+ return { content, binary: false, size: info.size, encoding: "utf-8" };
5673
+ }
5674
+ const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
5675
+ if (imageMimeType) {
5676
+ if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
5677
+ return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
5678
+ }
5679
+ const content = await readFile(resolved, "base64");
5680
+ return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
4846
5681
  }
4847
- if (info.size > 1048576) throw new Error("File too large");
4848
- const content = await readFile(resolved, "utf-8");
4849
- return { content, binary: false };
5682
+ return { content: null, binary: true, size: info.size };
4850
5683
  }
4851
5684
  // Skill scanning
4852
5685
  // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
@@ -4866,7 +5699,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4866
5699
  async listSkills(agentId, runtimeHint) {
4867
5700
  const agent = this.agents.get(agentId);
4868
5701
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4869
- const home = os4.homedir();
5702
+ const home = os5.homedir();
4870
5703
  const workspaceDir = path11.join(this.dataDir, agentId);
4871
5704
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4872
5705
  const globalResults = await Promise.all(
@@ -5096,7 +5929,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5096
5929
  this.clearCompactionWatchdog(ap);
5097
5930
  this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
5098
5931
  }
5099
- startRuntimeTrace(agentId, ap, reason) {
5932
+ messagesTraceAttrs(messages) {
5933
+ if (!messages || messages.length === 0) return {};
5934
+ const first = messages[0];
5935
+ const context = this.getDeliveryTraceContext(first);
5936
+ const notification = runtimeProfileNotificationFromMessage(first);
5937
+ if (notification) {
5938
+ return {
5939
+ messages_count: messages.length,
5940
+ message_id_present: Boolean(first.message_id),
5941
+ deliveryId: context.deliveryId,
5942
+ delivery_correlation_id: context.deliveryId,
5943
+ control_kind: notification.kind,
5944
+ key_present: Boolean(notification.key)
5945
+ };
5946
+ }
5947
+ return {
5948
+ messages_count: messages.length,
5949
+ messageId: first.message_id,
5950
+ message_id_present: Boolean(first.message_id),
5951
+ deliveryId: context.deliveryId,
5952
+ delivery_correlation_id: context.deliveryId ?? first.message_id
5953
+ };
5954
+ }
5955
+ startRuntimeTrace(agentId, ap, reason, messages) {
5100
5956
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
5101
5957
  const span = this.tracer.startSpan("daemon.runtime.turn", {
5102
5958
  surface: "daemon",
@@ -5106,10 +5962,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5106
5962
  runtime: ap.config.runtime,
5107
5963
  model: ap.config.model,
5108
5964
  reason,
5109
- hasSession: Boolean(ap.sessionId)
5965
+ hasSession: Boolean(ap.sessionId),
5966
+ ...this.messagesTraceAttrs(messages)
5110
5967
  }
5111
5968
  });
5112
- span.addEvent("daemon.turn.started", { reason });
5969
+ span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
5113
5970
  ap.runtimeTraceSpan = span;
5114
5971
  return span;
5115
5972
  }
@@ -5208,17 +6065,59 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5208
6065
  if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
5209
6066
  ap.runtimeProgressStaleSince = Date.now();
5210
6067
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
5211
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
6068
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
6069
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", diagnostic.traceAttrs);
6070
+ this.endRuntimeTrace(ap, "error", {
6071
+ outcome: "runtime-stalled",
5212
6072
  ageMs: staleForMs,
5213
- staleForMinutes,
5214
- lastActivity: ap.lastActivity
6073
+ lastActivity: ap.lastActivity,
6074
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6075
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
6076
+ });
6077
+ this.broadcastActivity(agentId, "error", diagnostic.detail);
6078
+ return true;
6079
+ }
6080
+ recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
6081
+ if (ap.inbox.length === 0) return false;
6082
+ if (ap.expectedTerminationReason === "stalled_recovery") {
6083
+ return true;
6084
+ }
6085
+ const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
6086
+ const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
6087
+ const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
6088
+ if (!canRestartStalledProcess) return false;
6089
+ const staleForMs = Date.now() - ap.lastRuntimeEventAt;
6090
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
6091
+ const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
6092
+ ap.runtimeProgressStaleSince ??= Date.now();
6093
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
6094
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
6095
+ ...diagnostic.traceAttrs,
6096
+ pendingMessages: ap.inbox.length,
6097
+ recovery: "terminate_for_queued_message"
5215
6098
  });
5216
6099
  this.endRuntimeTrace(ap, "error", {
5217
6100
  outcome: "runtime-stalled",
5218
6101
  ageMs: staleForMs,
5219
- lastActivity: ap.lastActivity
6102
+ lastActivity: ap.lastActivity,
6103
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6104
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6105
+ pendingMessages: ap.inbox.length,
6106
+ recovery: "terminate_for_queued_message"
5220
6107
  });
5221
- this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
6108
+ ap.expectedTerminationReason = "stalled_recovery";
6109
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
6110
+ logger.warn(
6111
+ `[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
6112
+ );
6113
+ this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
6114
+ try {
6115
+ ap.process.kill("SIGTERM");
6116
+ } catch (err) {
6117
+ const reason = err instanceof Error ? err.message : String(err);
6118
+ logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
6119
+ return false;
6120
+ }
5222
6121
  return true;
5223
6122
  }
5224
6123
  /** Handle a single ParsedEvent from any runtime driver */
@@ -5322,7 +6221,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5322
6221
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
5323
6222
  this.flushPendingTrajectory(agentId);
5324
6223
  if (ap) {
5325
- this.releaseAgentStartPermit(agentId, "initial turn ended");
5326
6224
  this.clearGatedInFlightBatch(agentId, ap, "turn_end");
5327
6225
  if (event.sessionId) ap.sessionId = event.sessionId;
5328
6226
  ap.gatedSteering.outstandingToolUses = 0;
@@ -5345,7 +6243,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5345
6243
  }
5346
6244
  } else {
5347
6245
  ap.isIdle = true;
5348
- this.broadcastActivity(agentId, "online", "Idle");
6246
+ if (ap.lastRuntimeError) {
6247
+ this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
6248
+ } else {
6249
+ this.broadcastActivity(agentId, "online", "Idle");
6250
+ }
5349
6251
  }
5350
6252
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
5351
6253
  if (ap.driver.terminateProcessOnTurnEnd) {
@@ -5384,6 +6286,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5384
6286
  }
5385
6287
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
5386
6288
  this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6289
+ if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6290
+ ap.isIdle = true;
6291
+ ap.pendingNotificationCount = 0;
6292
+ if (ap.notificationTimer) {
6293
+ clearTimeout(ap.notificationTimer);
6294
+ ap.notificationTimer = null;
6295
+ }
6296
+ logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
6297
+ }
5387
6298
  }
5388
6299
  this.broadcastActivity(agentId, "error", event.message, [
5389
6300
  { kind: "text", text: `Error: ${event.message}` }
@@ -5430,20 +6341,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5430
6341
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
5431
6342
  if (encoded) {
5432
6343
  ap.process.stdin?.write(encoded + "\n");
6344
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6345
+ agentId,
6346
+ runtime: ap.config.runtime,
6347
+ model: ap.config.model,
6348
+ launchId: ap.launchId || void 0,
6349
+ outcome: "written",
6350
+ mode: "busy",
6351
+ pending_notification_count: count,
6352
+ session_id_present: true
6353
+ });
6354
+ } else {
6355
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6356
+ agentId,
6357
+ runtime: ap.config.runtime,
6358
+ model: ap.config.model,
6359
+ launchId: ap.launchId || void 0,
6360
+ outcome: "encode_failed",
6361
+ mode: "busy",
6362
+ pending_notification_count: count,
6363
+ session_id_present: true
6364
+ }, "error");
5433
6365
  }
5434
6366
  }
5435
6367
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
5436
6368
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
5437
6369
  if (messages.length === 0) return true;
6370
+ const split = this.splitRuntimeProfileControlBatch(messages);
6371
+ if (split.deferredMessages.length > 0) {
6372
+ ap.inbox.unshift(...split.deferredMessages);
6373
+ ap.pendingNotificationCount += split.deferredMessages.length;
6374
+ messages = split.nextMessages;
6375
+ this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
6376
+ agentId,
6377
+ launchId: ap.launchId || void 0,
6378
+ runtime: ap.config.runtime,
6379
+ mode,
6380
+ delivered_control_messages_count: messages.length,
6381
+ deferred_messages_count: split.deferredMessages.length,
6382
+ inbox_count: ap.inbox.length,
6383
+ pending_notification_count: ap.pendingNotificationCount
6384
+ });
6385
+ }
6386
+ const traceAttrs = {
6387
+ agentId,
6388
+ launchId: ap.launchId || void 0,
6389
+ runtime: ap.config.runtime,
6390
+ model: ap.config.model,
6391
+ mode,
6392
+ messages_count: messages.length,
6393
+ session_id_present: Boolean(ap.sessionId),
6394
+ inbox_count: ap.inbox.length,
6395
+ pending_notification_count: ap.pendingNotificationCount,
6396
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
6397
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
6398
+ ...this.messagesTraceAttrs(messages)
6399
+ };
5438
6400
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
5439
6401
 
5440
6402
  ${formatIncomingMessage(messages[0], ap.driver)}
5441
6403
 
5442
- Respond as appropriate. Complete all your work before stopping.` : `New messages received:
6404
+ Respond as appropriate. Complete all your work before stopping.
6405
+ ${RESPONSE_TARGET_HINT}` : `New messages received:
5443
6406
 
5444
6407
  ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
5445
6408
 
5446
- Respond as appropriate. Complete all your work before stopping.`);
6409
+ Respond as appropriate. Complete all your work before stopping.
6410
+ ${RESPONSE_TARGET_HINT}`);
5447
6411
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
5448
6412
  if (!encoded) {
5449
6413
  ap.inbox.unshift(...messages);
@@ -5453,14 +6417,27 @@ Respond as appropriate. Complete all your work before stopping.`);
5453
6417
  logger.warn(
5454
6418
  `[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
5455
6419
  );
6420
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6421
+ ...traceAttrs,
6422
+ outcome: "encode_failed",
6423
+ requeued_messages_count: messages.length
6424
+ }, "error");
5456
6425
  return false;
5457
6426
  }
5458
6427
  const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
5459
6428
  logger.info(
5460
6429
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
5461
6430
  );
6431
+ if (this.containsOrdinaryInboxMessage(messages)) {
6432
+ ap.lastRuntimeError = null;
6433
+ }
5462
6434
  ap.process.stdin?.write(encoded + "\n");
5463
6435
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
6436
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6437
+ ...traceAttrs,
6438
+ outcome: "written",
6439
+ stdin_write_attempted: true
6440
+ });
5464
6441
  return true;
5465
6442
  }
5466
6443
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -5505,6 +6482,16 @@ var systemClock = {
5505
6482
  clearTimeout: (timer) => clearTimeout(timer)
5506
6483
  };
5507
6484
  var INBOUND_WATCHDOG_MS = 7e4;
6485
+ function durationMsBucket(ms) {
6486
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
6487
+ if (ms === 0) return "0";
6488
+ if (ms <= 1e3) return "1s";
6489
+ if (ms <= 1e4) return "1s-10s";
6490
+ if (ms <= 3e4) return "10s-30s";
6491
+ if (ms <= 6e4) return "30s-60s";
6492
+ if (ms <= 12e4) return "60s-120s";
6493
+ return "120s+";
6494
+ }
5508
6495
  var DaemonConnection = class {
5509
6496
  ws = null;
5510
6497
  options;
@@ -5516,6 +6503,8 @@ var DaemonConnection = class {
5516
6503
  shouldConnect = true;
5517
6504
  reconnectAttempt = 0;
5518
6505
  lastDroppedSendLogAt = 0;
6506
+ lastInboundAt = null;
6507
+ lastInboundMessageKind = null;
5519
6508
  constructor(options) {
5520
6509
  this.options = options;
5521
6510
  this.clock = options.clock ?? systemClock;
@@ -5550,6 +6539,10 @@ var DaemonConnection = class {
5550
6539
  this.lastDroppedSendLogAt = now;
5551
6540
  logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
5552
6541
  }
6542
+ this.trace("daemon.connection.outbound_dropped", {
6543
+ message_type: msg.type,
6544
+ ws_ready_state: this.ws?.readyState ?? null
6545
+ });
5553
6546
  }
5554
6547
  get connected() {
5555
6548
  return this.ws?.readyState === WebSocket.OPEN;
@@ -5563,25 +6556,49 @@ var DaemonConnection = class {
5563
6556
  if (wsOptions?.agent) {
5564
6557
  logger.info("[Daemon] Using configured proxy for WebSocket connection");
5565
6558
  }
6559
+ this.trace("daemon.connection.connecting", {
6560
+ reconnect_attempt: this.reconnectAttempt,
6561
+ server_url_present: Boolean(this.options.serverUrl),
6562
+ proxy_present: Boolean(wsOptions?.agent)
6563
+ });
5566
6564
  const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
5567
6565
  this.ws = ws;
5568
6566
  ws.on("open", () => {
5569
6567
  if (this.ws !== ws) return;
5570
6568
  if (!this.shouldConnect) return;
5571
6569
  logger.info("[Daemon] Connected to server");
6570
+ const priorReconnectAttempt = this.reconnectAttempt;
5572
6571
  this.reconnectAttempt = 0;
5573
6572
  this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
6573
+ this.markInbound("websocket_open");
5574
6574
  this.resetWatchdog();
6575
+ this.trace("daemon.connection.connected", {
6576
+ reconnect_attempt: priorReconnectAttempt,
6577
+ inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
6578
+ });
5575
6579
  this.options.onConnect();
5576
6580
  });
5577
6581
  ws.on("message", (data) => {
5578
6582
  if (this.ws !== ws) return;
5579
- this.resetWatchdog();
6583
+ let messageKind = "unknown";
5580
6584
  try {
5581
6585
  const msg = JSON.parse(data.toString());
6586
+ messageKind = msg.type;
6587
+ this.markInbound(messageKind);
6588
+ this.resetWatchdog();
6589
+ this.trace("daemon.connection.inbound_received", {
6590
+ message_type: messageKind,
6591
+ last_inbound_age_ms_bucket: "0"
6592
+ });
5582
6593
  this.options.onMessage(msg);
5583
6594
  } catch (err) {
6595
+ this.markInbound("invalid_json");
6596
+ this.resetWatchdog();
5584
6597
  logger.error("[Daemon] Invalid message from server", err);
6598
+ this.trace("daemon.connection.invalid_message", {
6599
+ error_class: err instanceof Error ? err.name : typeof err,
6600
+ last_inbound_message_kind: "invalid_json"
6601
+ }, "error");
5585
6602
  }
5586
6603
  });
5587
6604
  ws.on("close", (code, reasonBuffer) => {
@@ -5592,12 +6609,23 @@ var DaemonConnection = class {
5592
6609
  logger.warn(
5593
6610
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
5594
6611
  );
6612
+ this.trace("daemon.connection.disconnected", {
6613
+ close_code: code,
6614
+ close_reason_present: Boolean(reason),
6615
+ reconnecting: this.shouldConnect,
6616
+ reconnect_attempt: this.reconnectAttempt,
6617
+ last_inbound_message_kind: this.lastInboundMessageKind,
6618
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
6619
+ }, this.shouldConnect ? "cancelled" : "ok");
5595
6620
  this.options.onDisconnect();
5596
6621
  this.scheduleReconnect();
5597
6622
  });
5598
6623
  ws.on("error", (err) => {
5599
6624
  if (this.ws !== ws) return;
5600
6625
  logger.error(`[Daemon] WebSocket error: ${err.message}`);
6626
+ this.trace("daemon.connection.error", {
6627
+ error_class: err.name || "Error"
6628
+ }, "error");
5601
6629
  });
5602
6630
  }
5603
6631
  scheduleReconnect() {
@@ -5605,6 +6633,10 @@ var DaemonConnection = class {
5605
6633
  if (this.reconnectTimer) return;
5606
6634
  this.reconnectAttempt += 1;
5607
6635
  logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
6636
+ this.trace("daemon.connection.reconnect_scheduled", {
6637
+ reconnect_attempt: this.reconnectAttempt,
6638
+ delay_ms: this.reconnectDelay
6639
+ });
5608
6640
  this.reconnectTimer = this.clock.setTimeout(() => {
5609
6641
  this.reconnectTimer = null;
5610
6642
  this.doConnect();
@@ -5616,6 +6648,13 @@ var DaemonConnection = class {
5616
6648
  const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
5617
6649
  this.watchdogTimer = this.clock.setTimeout(() => {
5618
6650
  logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
6651
+ this.trace("daemon.connection.watchdog_timeout", {
6652
+ inbound_watchdog_ms: ms,
6653
+ last_inbound_message_kind: this.lastInboundMessageKind,
6654
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
6655
+ ws_ready_state: this.ws?.readyState ?? null,
6656
+ reconnecting: this.shouldConnect
6657
+ }, "error");
5619
6658
  try {
5620
6659
  this.ws?.terminate();
5621
6660
  } catch {
@@ -5628,6 +6667,16 @@ var DaemonConnection = class {
5628
6667
  this.watchdogTimer = null;
5629
6668
  }
5630
6669
  }
6670
+ markInbound(messageKind) {
6671
+ this.lastInboundAt = this.clock.now();
6672
+ this.lastInboundMessageKind = messageKind;
6673
+ }
6674
+ lastInboundAgeBucket() {
6675
+ return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
6676
+ }
6677
+ trace(name, attrs, status = "ok") {
6678
+ this.options.onTraceEvent?.(name, attrs, status);
6679
+ }
5631
6680
  };
5632
6681
 
5633
6682
  // src/reminderCache.ts
@@ -5711,10 +6760,10 @@ var ReminderCache = class {
5711
6760
 
5712
6761
  // src/machineLock.ts
5713
6762
  import { createHash, randomUUID as randomUUID2 } from "crypto";
5714
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
5715
- import os5 from "os";
6763
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
6764
+ import os6 from "os";
5716
6765
  import path12 from "path";
5717
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
6766
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
5718
6767
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
5719
6768
  var DaemonMachineLockConflictError = class extends Error {
5720
6769
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -5737,14 +6786,14 @@ function ownerPath(lockDir) {
5737
6786
  }
5738
6787
  function readOwner(lockDir) {
5739
6788
  try {
5740
- return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
6789
+ return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
5741
6790
  } catch {
5742
6791
  return null;
5743
6792
  }
5744
6793
  }
5745
6794
  function lockAgeMs(lockDir) {
5746
6795
  try {
5747
- return Date.now() - statSync2(lockDir).mtimeMs;
6796
+ return Date.now() - statSync3(lockDir).mtimeMs;
5748
6797
  } catch {
5749
6798
  return null;
5750
6799
  }
@@ -5773,7 +6822,7 @@ function acquireDaemonMachineLock(options) {
5773
6822
  const owner = {
5774
6823
  pid: process.pid,
5775
6824
  token,
5776
- hostname: os5.hostname(),
6825
+ hostname: os6.hostname(),
5777
6826
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5778
6827
  serverUrl: options.serverUrl,
5779
6828
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5815,6 +6864,418 @@ function acquireDaemonMachineLock(options) {
5815
6864
  throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
5816
6865
  }
5817
6866
 
6867
+ // src/localTraceSink.ts
6868
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6869
+ import path13 from "path";
6870
+ var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6871
+ var DEFAULT_MAX_FILES = 8;
6872
+ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6873
+ "serverId",
6874
+ "machineId",
6875
+ "agentId",
6876
+ "messageId",
6877
+ "launchId",
6878
+ "uploadId",
6879
+ "bundleId",
6880
+ "deliveryId",
6881
+ "deliveryCorrelationId",
6882
+ "delivery_correlation_id"
6883
+ ]);
6884
+ var LocalRotatingTraceSink = class {
6885
+ traceDir;
6886
+ maxFileBytes;
6887
+ maxFiles;
6888
+ currentFile = null;
6889
+ currentSize = 0;
6890
+ sequence = 0;
6891
+ constructor(options) {
6892
+ this.traceDir = path13.join(options.machineDir, "traces");
6893
+ this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6894
+ this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
6895
+ }
6896
+ record(span) {
6897
+ try {
6898
+ const line = `${JSON.stringify(toLocalTraceRecord(span))}
6899
+ `;
6900
+ this.ensureFile(Buffer.byteLength(line));
6901
+ appendFileSync(this.currentFile, line, { encoding: "utf8" });
6902
+ this.currentSize += Buffer.byteLength(line);
6903
+ } catch {
6904
+ }
6905
+ }
6906
+ getCurrentFile() {
6907
+ return this.currentFile;
6908
+ }
6909
+ ensureFile(nextBytes) {
6910
+ mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
6911
+ if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
6912
+ this.currentFile = path13.join(
6913
+ this.traceDir,
6914
+ `daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6915
+ );
6916
+ writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
6917
+ this.currentSize = statSync4(this.currentFile).size;
6918
+ this.pruneOldFiles();
6919
+ }
6920
+ }
6921
+ pruneOldFiles() {
6922
+ const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
6923
+ const excess = files.length - this.maxFiles;
6924
+ if (excess <= 0) return;
6925
+ for (const file of files.slice(0, excess)) {
6926
+ rmSync3(path13.join(this.traceDir, file), { force: true });
6927
+ }
6928
+ }
6929
+ };
6930
+ function safeTimestamp(timeMs) {
6931
+ return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
6932
+ }
6933
+ function toLocalTraceRecord(span) {
6934
+ return {
6935
+ type: "span",
6936
+ schema_version: 1,
6937
+ trace_id: span.context.traceId,
6938
+ span_id: span.context.spanId,
6939
+ parent_span_id: span.context.parentSpanId,
6940
+ name: span.name,
6941
+ surface: span.surface,
6942
+ kind: span.kind,
6943
+ status: span.status,
6944
+ start_time: new Date(span.startTimeMs).toISOString(),
6945
+ end_time: new Date(span.endTimeMs).toISOString(),
6946
+ duration_ms: span.durationMs,
6947
+ attrs: sanitizeAttrs(span.attrs),
6948
+ events: span.events.map(sanitizeEvent)
6949
+ };
6950
+ }
6951
+ function sanitizeEvent(event) {
6952
+ return {
6953
+ name: event.name,
6954
+ time: new Date(event.timeMs).toISOString(),
6955
+ attrs: sanitizeAttrs(event.attrs)
6956
+ };
6957
+ }
6958
+ function sanitizeAttrs(attrs) {
6959
+ if (!attrs) return void 0;
6960
+ const sanitized = {};
6961
+ for (const [key, value] of Object.entries(attrs)) {
6962
+ if (isDiagnosticIdAttr(key)) {
6963
+ if (value === null || value === void 0 || value === "") continue;
6964
+ sanitized[key] = sanitizeValue(value);
6965
+ continue;
6966
+ }
6967
+ if (shouldDropAttr(key)) continue;
6968
+ sanitized[key] = sanitizeValue(value);
6969
+ }
6970
+ return sanitized;
6971
+ }
6972
+ function sanitizeValue(value) {
6973
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6974
+ return value;
6975
+ }
6976
+ if (Array.isArray(value)) {
6977
+ return { items_count: value.length };
6978
+ }
6979
+ if (typeof value === "object") {
6980
+ return { object_present: true };
6981
+ }
6982
+ return String(value);
6983
+ }
6984
+ function shouldDropAttr(key) {
6985
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
6986
+ if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
6987
+ return true;
6988
+ }
6989
+ if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
6990
+ return false;
6991
+ }
6992
+ if (/(^|_)id$/.test(normalized)) {
6993
+ return true;
6994
+ }
6995
+ return /(^|_)(prompt|content|text|message|body|request|response|command|argv|env|cwd|path|file|error|tool_args|tool_input|tool_output|stdout|stderr)(_|$)/i.test(normalized);
6996
+ }
6997
+ function isDiagnosticIdAttr(key) {
6998
+ return DIAGNOSTIC_ID_ATTRS.has(key);
6999
+ }
7000
+
7001
+ // src/traceBundleUpload.ts
7002
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7003
+ import { gzipSync } from "zlib";
7004
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
7005
+ import path14 from "path";
7006
+
7007
+ // src/directUploadCapability.ts
7008
+ function joinUrl(base, path16) {
7009
+ return `${base.replace(/\/+$/, "")}${path16}`;
7010
+ }
7011
+ function jsonHeaders(apiKey) {
7012
+ return {
7013
+ "Content-Type": "application/json",
7014
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
7015
+ };
7016
+ }
7017
+ async function requestDaemonScopeAttestation({
7018
+ serverUrl,
7019
+ apiKey,
7020
+ scope,
7021
+ metadata,
7022
+ fetchImpl = fetch,
7023
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7024
+ }) {
7025
+ const { response, data } = await executeJsonRequest(
7026
+ joinUrl(serverUrl, "/internal/machine/scope-attestation"),
7027
+ {
7028
+ method: "POST",
7029
+ headers: jsonHeaders(apiKey),
7030
+ body: JSON.stringify({
7031
+ scope,
7032
+ ...metadata ? { metadata } : {}
7033
+ })
7034
+ },
7035
+ {
7036
+ toolName: "daemon_direct_upload.scope_attestation",
7037
+ target: scope,
7038
+ timeoutMs,
7039
+ fetchImpl
7040
+ }
7041
+ );
7042
+ if (!response.ok) {
7043
+ throw new Error(`Failed to request daemon scope attestation (${response.status})`);
7044
+ }
7045
+ return data;
7046
+ }
7047
+ async function createDirectUploadSession({
7048
+ serverUrl,
7049
+ apiKey,
7050
+ workerUrl,
7051
+ scope,
7052
+ createPath = "/api/uploads",
7053
+ body,
7054
+ attestationMetadata,
7055
+ fetchImpl = fetch,
7056
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7057
+ }) {
7058
+ const capability = await requestDaemonScopeAttestation({
7059
+ serverUrl,
7060
+ apiKey,
7061
+ scope,
7062
+ metadata: attestationMetadata,
7063
+ fetchImpl,
7064
+ timeoutMs
7065
+ });
7066
+ const { response, data } = await executeJsonRequest(
7067
+ joinUrl(workerUrl, createPath),
7068
+ {
7069
+ method: "POST",
7070
+ headers: jsonHeaders(),
7071
+ body: JSON.stringify({
7072
+ ...body,
7073
+ attestation: capability.attestation
7074
+ })
7075
+ },
7076
+ {
7077
+ toolName: "daemon_direct_upload.create",
7078
+ target: capability.audience,
7079
+ timeoutMs,
7080
+ fetchImpl
7081
+ }
7082
+ );
7083
+ if (!response.ok) {
7084
+ throw new Error(`Failed to create direct upload session (${response.status})`);
7085
+ }
7086
+ return { capability, response: data };
7087
+ }
7088
+ async function uploadWithSignedCapability({
7089
+ serverUrl,
7090
+ apiKey,
7091
+ workerUrl,
7092
+ scope,
7093
+ createPath = "/api/uploads",
7094
+ createBody,
7095
+ attestationMetadata,
7096
+ uploadBody,
7097
+ fetchImpl = fetch,
7098
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7099
+ }) {
7100
+ const { capability, response: session } = await createDirectUploadSession({
7101
+ serverUrl,
7102
+ apiKey,
7103
+ workerUrl,
7104
+ scope,
7105
+ createPath,
7106
+ body: createBody,
7107
+ attestationMetadata,
7108
+ fetchImpl,
7109
+ timeoutMs
7110
+ });
7111
+ const { response: uploadResponse } = await executeResponseRequest(
7112
+ session.upload.url,
7113
+ {
7114
+ method: session.upload.method,
7115
+ headers: session.upload.headers ?? {},
7116
+ body: uploadBody
7117
+ },
7118
+ {
7119
+ toolName: "daemon_direct_upload.put",
7120
+ target: capability.audience,
7121
+ timeoutMs,
7122
+ fetchImpl
7123
+ }
7124
+ );
7125
+ if (!uploadResponse.ok) {
7126
+ throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
7127
+ }
7128
+ return { capability, session, uploadResponse };
7129
+ }
7130
+
7131
+ // src/traceBundleUpload.ts
7132
+ var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
7133
+ var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
7134
+ var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
7135
+ var DEFAULT_MAX_FILES_PER_RUN = 4;
7136
+ var DaemonTraceBundleUploader = class {
7137
+ options;
7138
+ timer = null;
7139
+ constructor(options) {
7140
+ this.options = options;
7141
+ }
7142
+ start() {
7143
+ if (this.timer) return;
7144
+ void this.uploadOnce();
7145
+ this.timer = setInterval(() => {
7146
+ void this.uploadOnce();
7147
+ }, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
7148
+ }
7149
+ stop() {
7150
+ if (!this.timer) return;
7151
+ clearInterval(this.timer);
7152
+ this.timer = null;
7153
+ }
7154
+ async uploadOnce() {
7155
+ const files = await this.findUploadCandidates();
7156
+ let uploaded = 0;
7157
+ for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
7158
+ if (await this.uploadFile(file)) uploaded += 1;
7159
+ }
7160
+ return { attempted: files.length, uploaded };
7161
+ }
7162
+ async findUploadCandidates() {
7163
+ const traceDir = path14.join(this.options.machineDir, "traces");
7164
+ let names;
7165
+ try {
7166
+ names = await readdir3(traceDir);
7167
+ } catch {
7168
+ return [];
7169
+ }
7170
+ const now = Date.now();
7171
+ const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
7172
+ const currentFile = this.options.currentFileProvider?.();
7173
+ const candidates = [];
7174
+ for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
7175
+ const file = path14.join(traceDir, name);
7176
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
7177
+ if (await this.isUploaded(file)) continue;
7178
+ try {
7179
+ const info = await stat3(file);
7180
+ if (!info.isFile() || info.size <= 0) continue;
7181
+ if (now - info.mtimeMs < minAgeMs) continue;
7182
+ candidates.push(file);
7183
+ } catch {
7184
+ }
7185
+ }
7186
+ return candidates;
7187
+ }
7188
+ async uploadFile(file) {
7189
+ const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
7190
+ surface: "daemon",
7191
+ kind: "producer",
7192
+ attrs: {
7193
+ file_present: true,
7194
+ worker_url_present: Boolean(this.options.workerUrl)
7195
+ }
7196
+ });
7197
+ try {
7198
+ const raw = await readFile2(file);
7199
+ if (raw.byteLength === 0) {
7200
+ span?.end("cancelled", { attrs: { outcome: "empty" } });
7201
+ return false;
7202
+ }
7203
+ const gzipped = gzipSync(raw);
7204
+ const bundleSha256 = sha256Hex(gzipped);
7205
+ const bundleId = randomUUID3();
7206
+ await uploadWithSignedCapability({
7207
+ serverUrl: this.options.serverUrl,
7208
+ apiKey: this.options.apiKey,
7209
+ workerUrl: this.options.workerUrl,
7210
+ scope: TRACE_UPLOAD_SCOPE,
7211
+ createPath: "/api/trace-bundles",
7212
+ attestationMetadata: {
7213
+ bundleId,
7214
+ bundleSha256,
7215
+ bundleSizeBytes: gzipped.byteLength
7216
+ },
7217
+ createBody: {
7218
+ bundleSha256,
7219
+ bundleSizeBytes: gzipped.byteLength
7220
+ },
7221
+ uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
7222
+ fetchImpl: this.options.fetchImpl
7223
+ });
7224
+ await this.markUploaded(file, {
7225
+ bundleId,
7226
+ bundleSha256,
7227
+ bundleSizeBytes: gzipped.byteLength
7228
+ });
7229
+ span?.end("ok", {
7230
+ attrs: {
7231
+ bundleId,
7232
+ bundle_size_bytes: gzipped.byteLength
7233
+ }
7234
+ });
7235
+ return true;
7236
+ } catch (err) {
7237
+ span?.end("error", {
7238
+ attrs: {
7239
+ error_class: err instanceof Error ? err.name : "Error",
7240
+ error_message_present: err instanceof Error && Boolean(err.message)
7241
+ }
7242
+ });
7243
+ return false;
7244
+ }
7245
+ }
7246
+ uploadStatePath(file) {
7247
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7248
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
7249
+ }
7250
+ async isUploaded(file) {
7251
+ try {
7252
+ await stat3(this.uploadStatePath(file));
7253
+ return true;
7254
+ } catch {
7255
+ return false;
7256
+ }
7257
+ }
7258
+ async markUploaded(file, metadata) {
7259
+ const stateFile = this.uploadStatePath(file);
7260
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
7261
+ await writeFile2(stateFile, `${JSON.stringify({
7262
+ file: path14.basename(file),
7263
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7264
+ ...metadata
7265
+ }, null, 2)}
7266
+ `, { mode: 384 });
7267
+ }
7268
+ };
7269
+ function sha256Hex(body) {
7270
+ return createHash2("sha256").update(body).digest("hex");
7271
+ }
7272
+ function readPositiveIntegerEnv2(name, fallback) {
7273
+ const value = process.env[name];
7274
+ if (!value) return fallback;
7275
+ const parsed = Number(value);
7276
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
7277
+ }
7278
+
5818
7279
  // src/core.ts
5819
7280
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
5820
7281
  function parseDaemonCliArgs(args) {
@@ -5836,59 +7297,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5836
7297
  }
5837
7298
  }
5838
7299
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5839
- const dirname = path13.dirname(fileURLToPath(moduleUrl));
5840
- const jsPath = path13.resolve(dirname, "chat-bridge.js");
7300
+ const dirname = path15.dirname(fileURLToPath(moduleUrl));
7301
+ const jsPath = path15.resolve(dirname, "chat-bridge.js");
5841
7302
  try {
5842
7303
  accessSync(jsPath);
5843
7304
  return jsPath;
5844
7305
  } catch {
5845
- return path13.resolve(dirname, "chat-bridge.ts");
7306
+ return path15.resolve(dirname, "chat-bridge.ts");
5846
7307
  }
5847
7308
  }
5848
7309
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5849
- const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5850
- const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
7310
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7311
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
5851
7312
  try {
5852
7313
  accessSync(bundledDistPath);
5853
7314
  return bundledDistPath;
5854
7315
  } catch {
5855
- const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7316
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5856
7317
  accessSync(workspaceDistPath);
5857
7318
  return workspaceDistPath;
5858
7319
  }
5859
7320
  }
5860
- function detectRuntimes() {
7321
+ function detectRuntimes(tracer = noopTracer) {
5861
7322
  const ids = [];
5862
7323
  const versions = {};
7324
+ const span = tracer.startSpan("daemon.runtime.detect", {
7325
+ surface: "daemon",
7326
+ kind: "internal",
7327
+ attrs: {
7328
+ known_runtime_count: RUNTIMES.length
7329
+ }
7330
+ });
5863
7331
  for (const runtime of RUNTIMES) {
5864
7332
  const driver = getDriver(runtime.id);
7333
+ let probeErrorPresent = false;
5865
7334
  try {
5866
7335
  if (driver.probe) {
5867
7336
  const probe = driver.probe();
5868
7337
  if (!probe.available) {
5869
7338
  if (probe.version) versions[runtime.id] = probe.version;
7339
+ span.addEvent("daemon.runtime.detect.checked", {
7340
+ runtime: runtime.id,
7341
+ outcome: "unavailable",
7342
+ version_present: Boolean(probe.version),
7343
+ binary_path_present: false
7344
+ });
5870
7345
  continue;
5871
7346
  }
5872
7347
  ids.push(runtime.id);
5873
7348
  if (probe.version) versions[runtime.id] = probe.version;
7349
+ span.addEvent("daemon.runtime.detect.checked", {
7350
+ runtime: runtime.id,
7351
+ outcome: "available",
7352
+ version_present: Boolean(probe.version),
7353
+ binary_path_present: false
7354
+ });
5874
7355
  continue;
5875
7356
  }
5876
7357
  } catch {
7358
+ probeErrorPresent = true;
5877
7359
  }
5878
7360
  const detectionBinaries = [runtime.binary];
7361
+ let detectedByPath = false;
5879
7362
  for (const binary of detectionBinaries) {
5880
7363
  const resolved = resolveCommandOnPath(binary);
5881
7364
  if (!resolved) continue;
5882
7365
  ids.push(runtime.id);
7366
+ detectedByPath = true;
5883
7367
  const version = readCommandVersion(binary);
5884
7368
  if (version) {
5885
7369
  versions[runtime.id] = version;
5886
7370
  }
7371
+ span.addEvent("daemon.runtime.detect.checked", {
7372
+ runtime: runtime.id,
7373
+ outcome: "available",
7374
+ version_present: Boolean(version),
7375
+ binary_path_present: true,
7376
+ probe_error_present: probeErrorPresent
7377
+ });
5887
7378
  break;
5888
7379
  }
7380
+ if (!detectedByPath) {
7381
+ span.addEvent("daemon.runtime.detect.checked", {
7382
+ runtime: runtime.id,
7383
+ outcome: "unavailable",
7384
+ version_present: false,
7385
+ binary_path_present: false,
7386
+ probe_error_present: probeErrorPresent
7387
+ });
7388
+ }
5889
7389
  }
7390
+ span.end("ok", {
7391
+ attrs: {
7392
+ detected_runtime_count: ids.length
7393
+ }
7394
+ });
5890
7395
  return { ids, versions };
5891
7396
  }
7397
+ function readPositiveIntegerEnv3(name, fallback) {
7398
+ const raw = process.env[name];
7399
+ if (!raw) return fallback;
7400
+ const parsed = Number(raw);
7401
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
7402
+ return Math.floor(parsed);
7403
+ }
5892
7404
  function formatChannelTarget(msg) {
5893
7405
  return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
5894
7406
  }
@@ -5938,14 +7450,18 @@ var DaemonCore = class {
5938
7450
  connection;
5939
7451
  reminderCache;
5940
7452
  tracer;
7453
+ injectedTracer;
5941
7454
  machineLock = null;
7455
+ localTraceSink = null;
7456
+ traceBundleUploader = null;
5942
7457
  constructor(options) {
5943
7458
  this.options = options;
5944
7459
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
5945
7460
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
5946
7461
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
5947
- this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
7462
+ this.injectedTracer = Boolean(options.tracer);
5948
7463
  this.tracer = options.tracer ?? noopTracer;
7464
+ this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
5949
7465
  this.reminderCache = new ReminderCache({
5950
7466
  clock: options.reminderClock,
5951
7467
  onFire: (job) => this.onReminderFire(job)
@@ -5955,7 +7471,8 @@ var DaemonCore = class {
5955
7471
  dataDir: options.dataDir,
5956
7472
  serverUrl: options.serverUrl,
5957
7473
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
5958
- slockCliPath: this.slockCliPath
7474
+ slockCliPath: this.slockCliPath,
7475
+ tracer: this.tracer
5959
7476
  };
5960
7477
  this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
5961
7478
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -5965,15 +7482,48 @@ var DaemonCore = class {
5965
7482
  ...options.connectionOptions,
5966
7483
  onMessage: (msg) => this.handleMessage(msg),
5967
7484
  onConnect: () => this.handleConnect(),
5968
- onDisconnect: () => this.handleDisconnect()
7485
+ onDisconnect: () => this.handleDisconnect(),
7486
+ onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
5969
7487
  });
5970
7488
  this.connection = connection;
5971
7489
  }
5972
7490
  resolveMachineStateRoot() {
5973
7491
  if (this.options.machineStateDir) return this.options.machineStateDir;
5974
- if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
7492
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
5975
7493
  return DEFAULT_MACHINE_STATE_ROOT;
5976
7494
  }
7495
+ shouldEnableLocalTrace() {
7496
+ if (this.injectedTracer) return false;
7497
+ if (!this.options.localTrace) return false;
7498
+ return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
7499
+ }
7500
+ installLocalTraceSink(machineDir) {
7501
+ if (!this.shouldEnableLocalTrace()) return;
7502
+ this.localTraceSink = new LocalRotatingTraceSink({
7503
+ machineDir,
7504
+ maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
7505
+ maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
7506
+ });
7507
+ this.tracer = new BasicTracer({
7508
+ sink: this.localTraceSink
7509
+ });
7510
+ this.agentManager.setTracer(this.tracer);
7511
+ }
7512
+ installTraceBundleUploader(machineDir) {
7513
+ if (!this.shouldEnableLocalTrace()) return;
7514
+ if (this.traceBundleUploader) return;
7515
+ const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
7516
+ if (!workerUrl) return;
7517
+ this.traceBundleUploader = new DaemonTraceBundleUploader({
7518
+ machineDir,
7519
+ serverUrl: this.options.serverUrl,
7520
+ apiKey: this.options.apiKey,
7521
+ workerUrl,
7522
+ tracer: this.tracer,
7523
+ currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
7524
+ });
7525
+ this.traceBundleUploader.start();
7526
+ }
5977
7527
  start() {
5978
7528
  logger.info("[Slock Daemon] Starting...");
5979
7529
  if (!this.machineLock) {
@@ -5983,10 +7533,24 @@ var DaemonCore = class {
5983
7533
  rootDir: this.resolveMachineStateRoot()
5984
7534
  });
5985
7535
  logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
7536
+ this.installLocalTraceSink(this.machineLock.machineDir);
7537
+ this.installTraceBundleUploader(this.machineLock.machineDir);
7538
+ const span = this.tracer.startSpan("daemon.lifecycle.start", {
7539
+ surface: "daemon",
7540
+ kind: "internal",
7541
+ attrs: {
7542
+ machine_dir_present: true,
7543
+ local_trace_enabled: this.shouldEnableLocalTrace()
7544
+ }
7545
+ });
7546
+ span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
7547
+ span.end("ok");
5986
7548
  }
5987
7549
  try {
5988
7550
  this.connection.connect();
5989
7551
  } catch (err) {
7552
+ this.traceBundleUploader?.stop();
7553
+ this.traceBundleUploader = null;
5990
7554
  this.machineLock.release();
5991
7555
  this.machineLock = null;
5992
7556
  throw err;
@@ -5994,13 +7558,24 @@ var DaemonCore = class {
5994
7558
  }
5995
7559
  async stop() {
5996
7560
  logger.info("[Slock Daemon] Shutting down...");
7561
+ const span = this.tracer.startSpan("daemon.lifecycle.stop", {
7562
+ surface: "daemon",
7563
+ kind: "internal",
7564
+ attrs: { machine_lock_present: Boolean(this.machineLock) }
7565
+ });
5997
7566
  this.reminderCache.clear();
7567
+ this.traceBundleUploader?.stop();
7568
+ this.traceBundleUploader = null;
5998
7569
  try {
5999
7570
  await this.agentManager.stopAll();
7571
+ span.addEvent("daemon.agents.stopped");
6000
7572
  } finally {
6001
7573
  this.connection.disconnect();
7574
+ span.addEvent("daemon.connection.disconnect_requested");
6002
7575
  this.machineLock?.release();
7576
+ if (this.machineLock) span.addEvent("daemon.machine_lock.released");
6003
7577
  this.machineLock = null;
7578
+ span.end("ok");
6004
7579
  }
6005
7580
  }
6006
7581
  get connected() {
@@ -6009,6 +7584,14 @@ var DaemonCore = class {
6009
7584
  getRunningAgentIds() {
6010
7585
  return this.agentManager.getRunningAgentIds();
6011
7586
  }
7587
+ recordDaemonTrace(name, attrs, status = "ok") {
7588
+ const span = this.tracer.startSpan(name, {
7589
+ surface: "daemon",
7590
+ kind: "internal",
7591
+ attrs
7592
+ });
7593
+ span.end(status);
7594
+ }
6012
7595
  handleMessage(msg) {
6013
7596
  const summary = summarizeIncomingMessage(msg);
6014
7597
  logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
@@ -6038,50 +7621,88 @@ var DaemonCore = class {
6038
7621
  kind: "consumer",
6039
7622
  attrs: {
6040
7623
  agentId: msg.agentId,
7624
+ deliveryId: msg.deliveryId,
7625
+ delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
7626
+ messageId: msg.message.message_id,
7627
+ message_id_present: Boolean(msg.message.message_id),
6041
7628
  seq: msg.seq
6042
7629
  }
6043
7630
  });
6044
7631
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
6045
7632
  try {
6046
- span.addEvent("daemon.receive", { seq: msg.seq });
6047
- this.agentManager.deliverMessage(msg.agentId, msg.message);
6048
- span.addEvent("daemon.deliver_to_agent_manager");
7633
+ span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
7634
+ const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
7635
+ span.addEvent("daemon.deliver_to_agent_manager", { accepted });
7636
+ if (!accepted) {
7637
+ span.end("ok", { attrs: { outcome: "not-accepted" } });
7638
+ break;
7639
+ }
6049
7640
  const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
6050
7641
  span.addEvent("daemon.ack.sent", { seq: ackSeq });
6051
7642
  this.connection.send({
6052
7643
  type: "agent:deliver:ack",
6053
7644
  agentId: msg.agentId,
6054
7645
  seq: ackSeq,
6055
- traceparent: formatTraceparent(span.context)
7646
+ traceparent: formatTraceparent(span.context),
7647
+ deliveryId: msg.deliveryId
6056
7648
  });
6057
- span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
7649
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
6058
7650
  } catch (err) {
6059
- span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
7651
+ span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
6060
7652
  throw err;
6061
7653
  }
6062
7654
  break;
6063
7655
  }
6064
- case "agent:runtime_profile:migration":
7656
+ case "agent:runtime_profile:migration": {
7657
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7658
+ parent: parseTraceparent(msg.traceparent),
7659
+ surface: "daemon",
7660
+ kind: "consumer",
7661
+ attrs: {
7662
+ agentId: msg.agentId,
7663
+ control_kind: "migration",
7664
+ key_present: Boolean(msg.migrationKey),
7665
+ launchId: msg.launchId || void 0
7666
+ }
7667
+ });
6065
7668
  logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
6066
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
7669
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
7670
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
6067
7671
  break;
6068
- case "agent:runtime_profile:daemon_release_notice":
7672
+ }
7673
+ case "agent:runtime_profile:daemon_release_notice": {
7674
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7675
+ parent: parseTraceparent(msg.traceparent),
7676
+ surface: "daemon",
7677
+ kind: "consumer",
7678
+ attrs: {
7679
+ agentId: msg.agentId,
7680
+ control_kind: "daemon_release_notice",
7681
+ key_present: Boolean(msg.noticeKey),
7682
+ launchId: msg.launchId || void 0
7683
+ }
7684
+ });
6069
7685
  logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
6070
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
7686
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
7687
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
6071
7688
  break;
7689
+ }
6072
7690
  case "agent:workspace:list":
6073
7691
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
6074
7692
  this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
6075
7693
  });
6076
7694
  break;
6077
7695
  case "agent:workspace:read":
6078
- this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
7696
+ this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
6079
7697
  this.connection.send({
6080
7698
  type: "agent:workspace:file_content",
6081
7699
  agentId: msg.agentId,
6082
7700
  requestId: msg.requestId,
6083
7701
  content,
6084
- binary
7702
+ binary,
7703
+ size,
7704
+ mimeType,
7705
+ encoding
6085
7706
  });
6086
7707
  }).catch(() => {
6087
7708
  this.connection.send({
@@ -6089,7 +7710,8 @@ var DaemonCore = class {
6089
7710
  agentId: msg.agentId,
6090
7711
  requestId: msg.requestId,
6091
7712
  content: null,
6092
- binary: false
7713
+ binary: false,
7714
+ size: 0
6093
7715
  });
6094
7716
  });
6095
7717
  break;
@@ -6165,35 +7787,59 @@ var DaemonCore = class {
6165
7787
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
6166
7788
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
6167
7789
  logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
7790
+ const runningAgentIds = this.agentManager.getRunningAgentIds();
7791
+ const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
7792
+ const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
6168
7793
  this.connection.send({
6169
7794
  type: "ready",
6170
7795
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
6171
7796
  runtimes,
6172
- runningAgents: this.agentManager.getRunningAgentIds(),
6173
- hostname: this.options.hostname ?? os6.hostname(),
6174
- os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
7797
+ runningAgents: runningAgentIds,
7798
+ hostname: this.options.hostname ?? os7.hostname(),
7799
+ os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
6175
7800
  daemonVersion: this.daemonVersion
6176
7801
  });
6177
- for (const agentId of this.agentManager.getRunningAgentIds()) {
7802
+ this.recordDaemonTrace("daemon.ready.sent", {
7803
+ runtimes_count: runtimes.length,
7804
+ running_agents_count: runningAgentIds.length,
7805
+ idle_agents_count: idleAgentSessions.length,
7806
+ runtime_profile_reports_count: runtimeProfileReports.length,
7807
+ daemon_version_present: Boolean(this.daemonVersion)
7808
+ });
7809
+ for (const agentId of runningAgentIds) {
6178
7810
  const sessionId = this.agentManager.getAgentSessionId(agentId);
6179
7811
  const launchId = this.agentManager.getAgentLaunchId(agentId);
6180
7812
  if (sessionId) {
6181
7813
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
6182
7814
  }
6183
7815
  }
6184
- for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
7816
+ for (const { agentId, sessionId, launchId } of idleAgentSessions) {
6185
7817
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
6186
7818
  }
6187
- for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
7819
+ for (const report of runtimeProfileReports) {
7820
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
7821
+ surface: "daemon",
7822
+ kind: "producer",
7823
+ attrs: {
7824
+ agentId: report.agentId,
7825
+ launchId: report.launchId || void 0,
7826
+ runtime: report.facts.runtime,
7827
+ model_present: Boolean(report.facts.model),
7828
+ session_ref_present: Boolean(report.facts.sessionRef),
7829
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
7830
+ }
7831
+ });
6188
7832
  this.connection.send({
6189
7833
  type: "agent:runtime_profile",
6190
7834
  agentId: report.agentId,
6191
7835
  facts: report.facts,
6192
- launchId: report.launchId || void 0
7836
+ launchId: report.launchId || void 0,
7837
+ traceparent: formatTraceparent(span.context)
6193
7838
  });
7839
+ span.end("ok");
6194
7840
  }
6195
- const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
6196
- for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
7841
+ const agentsForSnapshot = new Set(runningAgentIds);
7842
+ for (const { agentId } of idleAgentSessions) {
6197
7843
  agentsForSnapshot.add(agentId);
6198
7844
  }
6199
7845
  for (const agentId of agentsForSnapshot) {
@@ -6203,6 +7849,10 @@ var DaemonCore = class {
6203
7849
  }
6204
7850
  handleDisconnect() {
6205
7851
  logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
7852
+ this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
7853
+ running_agents_count: this.agentManager.getRunningAgentIds().length,
7854
+ idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
7855
+ }, "cancelled");
6206
7856
  this.options.lifecycleHooks?.onDisconnect?.();
6207
7857
  }
6208
7858
  };