@slock-ai/daemon 0.44.2 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
 
@@ -1301,6 +1363,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
1301
1363
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1302
1364
  var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1303
1365
  var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
1366
+ var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
1304
1367
  var CLAUDE_DISALLOWED_TOOLS = [
1305
1368
  "EnterPlanMode",
1306
1369
  "ExitPlanMode",
@@ -1326,6 +1389,80 @@ function probeClaude(deps = {}) {
1326
1389
  version: readCommandVersion(command, [], deps) ?? void 0
1327
1390
  };
1328
1391
  }
1392
+ function isRecord(value) {
1393
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1394
+ }
1395
+ function expandClaudeMcpConfigVariables(raw, vars) {
1396
+ let expanded = raw;
1397
+ for (const [name, value] of Object.entries(vars)) {
1398
+ expanded = expanded.replaceAll(`\${${name}}`, value);
1399
+ }
1400
+ return expanded;
1401
+ }
1402
+ function readClaudeMcpServers(configPath, vars = {}) {
1403
+ try {
1404
+ const parsed = JSON.parse(
1405
+ expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
1406
+ );
1407
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
1408
+ return parsed.mcpServers;
1409
+ } catch (err) {
1410
+ logger.warn(
1411
+ `[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
1412
+ );
1413
+ return null;
1414
+ }
1415
+ }
1416
+ function resolveClaudeConfigDir(ctx, home) {
1417
+ const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
1418
+ return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
1419
+ }
1420
+ function collectClaudeMcpConfigFiles(ctx, home) {
1421
+ const files = [];
1422
+ const pushIfFile = (candidate) => {
1423
+ try {
1424
+ if (existsSync2(candidate) && statSync(candidate).isFile()) {
1425
+ files.push({ path: candidate });
1426
+ }
1427
+ } catch {
1428
+ }
1429
+ };
1430
+ pushIfFile(path3.join(home, ".claude.json"));
1431
+ pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
1432
+ const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
1433
+ try {
1434
+ for (const entry of readdirSync(pluginRoot)) {
1435
+ const pluginPath = path3.join(pluginRoot, entry);
1436
+ const configPath = path3.join(pluginPath, ".mcp.json");
1437
+ try {
1438
+ if (existsSync2(configPath) && statSync(configPath).isFile()) {
1439
+ files.push({
1440
+ path: configPath,
1441
+ vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
1442
+ });
1443
+ }
1444
+ } catch {
1445
+ }
1446
+ }
1447
+ } catch {
1448
+ }
1449
+ return files;
1450
+ }
1451
+ function buildClaudeUserMcpServers(ctx, home) {
1452
+ const servers = /* @__PURE__ */ Object.create(null);
1453
+ for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
1454
+ const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
1455
+ if (!mcpServers) continue;
1456
+ for (const [name, server] of Object.entries(mcpServers)) {
1457
+ if (!isRecord(server)) {
1458
+ logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
1459
+ continue;
1460
+ }
1461
+ servers[name] = server;
1462
+ }
1463
+ }
1464
+ return servers;
1465
+ }
1329
1466
  var ClaudeDriver = class {
1330
1467
  id = "claude";
1331
1468
  lifecycle = {
@@ -1381,36 +1518,46 @@ var ClaudeDriver = class {
1381
1518
  }
1382
1519
  return args;
1383
1520
  }
1384
- buildRuntimeActionsMcpConfig(ctx) {
1521
+ buildRuntimeActionsMcpServer(ctx) {
1385
1522
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1386
1523
  const command = isTsSource ? "npx" : "node";
1387
1524
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
1525
+ return {
1526
+ command,
1527
+ args: [
1528
+ ...bridgeArgs,
1529
+ "--agent-id",
1530
+ ctx.agentId,
1531
+ "--server-url",
1532
+ ctx.config.serverUrl,
1533
+ "--auth-token",
1534
+ ctx.config.authToken || ctx.daemonApiKey,
1535
+ "--runtime",
1536
+ this.id,
1537
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1538
+ "--runtime-actions-only"
1539
+ ]
1540
+ };
1541
+ }
1542
+ buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
1543
+ const userMcpServers = buildClaudeUserMcpServers(ctx, home);
1544
+ if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
1545
+ logger.warn(
1546
+ `[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
1547
+ );
1548
+ }
1388
1549
  return JSON.stringify({
1389
1550
  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
- }
1551
+ ...userMcpServers,
1552
+ [SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
1406
1553
  }
1407
1554
  });
1408
1555
  }
1409
- writeClaudeLaunchFiles(ctx, slockDir) {
1556
+ writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
1410
1557
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1411
1558
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1412
1559
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1413
- writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1560
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
1414
1561
  return { systemPromptPath, mcpConfigPath };
1415
1562
  }
1416
1563
  spawn(ctx) {
@@ -1558,8 +1705,8 @@ var ClaudeDriver = class {
1558
1705
 
1559
1706
  // src/drivers/codex.ts
1560
1707
  import { spawn as spawn2, execSync } from "child_process";
1561
- import { existsSync as existsSync2, readFileSync } from "fs";
1562
- import os from "os";
1708
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1709
+ import os2 from "os";
1563
1710
  import path4 from "path";
1564
1711
  function getCodexNotificationErrorMessage(params) {
1565
1712
  const topLevelMessage = params?.message;
@@ -1573,7 +1720,7 @@ function getCodexNotificationErrorMessage(params) {
1573
1720
  return null;
1574
1721
  }
1575
1722
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1576
- const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1723
+ const existsSyncFn = deps.existsSyncFn ?? existsSync3;
1577
1724
  const execSyncFn = deps.execSyncFn ?? execSync;
1578
1725
  const gitDir = path4.join(workingDirectory, ".git");
1579
1726
  if (existsSyncFn(gitDir)) return;
@@ -1620,14 +1767,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1620
1767
  try {
1621
1768
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1622
1769
  const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1623
- if (existsSync2(candidate)) codexEntry = candidate;
1770
+ if (existsSync3(candidate)) codexEntry = candidate;
1624
1771
  } catch {
1625
1772
  }
1626
1773
  if (!codexEntry) {
1627
1774
  try {
1628
1775
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
1629
1776
  const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1630
- if (existsSync2(candidate)) codexEntry = candidate;
1777
+ if (existsSync3(candidate)) codexEntry = candidate;
1631
1778
  } catch {
1632
1779
  }
1633
1780
  }
@@ -2047,12 +2194,12 @@ var CodexDriver = class {
2047
2194
  return detectCodexModels();
2048
2195
  }
2049
2196
  };
2050
- function detectCodexModels(home = os.homedir()) {
2197
+ function detectCodexModels(home = os2.homedir()) {
2051
2198
  const cachePath = path4.join(home, ".codex", "models_cache.json");
2052
2199
  const configPath = path4.join(home, ".codex", "config.toml");
2053
2200
  let models = [];
2054
2201
  try {
2055
- const raw = readFileSync(cachePath, "utf8");
2202
+ const raw = readFileSync2(cachePath, "utf8");
2056
2203
  const parsed = JSON.parse(raw);
2057
2204
  const entries = Array.isArray(parsed?.models) ? parsed.models : [];
2058
2205
  for (const entry of entries) {
@@ -2069,7 +2216,7 @@ function detectCodexModels(home = os.homedir()) {
2069
2216
  if (models.length === 0) return null;
2070
2217
  let defaultModel;
2071
2218
  try {
2072
- const raw = readFileSync(configPath, "utf8");
2219
+ const raw = readFileSync2(configPath, "utf8");
2073
2220
  const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
2074
2221
  if (match) defaultModel = match[1];
2075
2222
  } catch {
@@ -2234,7 +2381,7 @@ var CopilotDriver = class {
2234
2381
 
2235
2382
  // src/drivers/cursor.ts
2236
2383
  import { spawn as spawn4, spawnSync } from "child_process";
2237
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2384
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2238
2385
  import path6 from "path";
2239
2386
  var CursorDriver = class {
2240
2387
  id = "cursor";
@@ -2260,7 +2407,7 @@ var CursorDriver = class {
2260
2407
  busyDeliveryMode = "none";
2261
2408
  spawn(ctx) {
2262
2409
  const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
2263
- if (!existsSync3(cursorDir)) {
2410
+ if (!existsSync4(cursorDir)) {
2264
2411
  mkdirSync2(cursorDir, { recursive: true });
2265
2412
  }
2266
2413
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
@@ -2412,18 +2559,14 @@ function runCursorModelsCommand() {
2412
2559
 
2413
2560
  // src/drivers/gemini.ts
2414
2561
  import { spawn as spawn5 } from "child_process";
2415
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2562
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2416
2563
  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
- };
2564
+ function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2565
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
2566
+ if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
2567
+ spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
2568
+ }
2569
+ return spawnEnv;
2427
2570
  }
2428
2571
  var GeminiDriver = class {
2429
2572
  id = "gemini";
@@ -2434,40 +2577,26 @@ var GeminiDriver = class {
2434
2577
  inFlightWake: "spawn_new"
2435
2578
  };
2436
2579
  communication = {
2437
- chat: "mcp_chat_bridge",
2580
+ chat: "slock_cli",
2438
2581
  runtimeControl: "mcp_runtime_actions"
2439
2582
  };
2440
2583
  session = {
2441
2584
  recovery: "resume_or_fresh"
2442
2585
  };
2443
2586
  model = {
2444
- detectedModelsVerifiedAs: "launchable",
2445
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2587
+ detectedModelsVerifiedAs: "suggestion_only",
2588
+ toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
2446
2589
  };
2447
2590
  supportsStdinNotification = false;
2448
2591
  mcpToolPrefix = "";
2449
2592
  busyDeliveryMode = "none";
2593
+ usesSlockCliForCommunication = true;
2450
2594
  sessionId = null;
2451
2595
  sessionAnnounced = false;
2452
2596
  spawn(ctx) {
2453
2597
  this.sessionId = ctx.config.sessionId || null;
2454
2598
  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");
2599
+ this.writeGeminiSettings(ctx);
2471
2600
  const args = [
2472
2601
  "--output-format",
2473
2602
  "stream-json",
@@ -2541,23 +2670,56 @@ var GeminiDriver = class {
2541
2670
  return null;
2542
2671
  }
2543
2672
  buildSystemPrompt(config, _agentId) {
2544
- return buildMcpSystemPrompt(config, {
2673
+ return buildCliTransportSystemPrompt(config, {
2545
2674
  toolPrefix: "",
2546
2675
  extraCriticalRules: [
2547
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
2676
+ "- 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."
2677
+ ],
2678
+ postStartupNotes: [
2679
+ "**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
2680
  ],
2549
- postStartupNotes: [],
2550
2681
  includeStdinNotificationSection: false,
2551
2682
  messageNotificationStyle: "poll"
2552
2683
  });
2553
2684
  }
2685
+ writeGeminiSettings(ctx) {
2686
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2687
+ mkdirSync3(geminiDir, { recursive: true });
2688
+ const settingsPath = path7.join(geminiDir, "settings.json");
2689
+ writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
2690
+ }
2691
+ buildRuntimeActionsMcpSettings(ctx) {
2692
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2693
+ const command = isTsSource ? "npx" : "node";
2694
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2695
+ return {
2696
+ mcpServers: {
2697
+ chat: {
2698
+ command,
2699
+ args: [
2700
+ ...bridgeArgs,
2701
+ "--agent-id",
2702
+ ctx.agentId,
2703
+ "--server-url",
2704
+ ctx.config.serverUrl,
2705
+ "--auth-token",
2706
+ ctx.config.authToken || ctx.daemonApiKey,
2707
+ "--runtime",
2708
+ this.id,
2709
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2710
+ "--runtime-actions-only"
2711
+ ]
2712
+ }
2713
+ }
2714
+ };
2715
+ }
2554
2716
  };
2555
2717
 
2556
2718
  // src/drivers/kimi.ts
2557
2719
  import { randomUUID } from "crypto";
2558
2720
  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";
2721
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2722
+ import os3 from "os";
2561
2723
  import path8 from "path";
2562
2724
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2563
2725
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
@@ -2773,11 +2935,11 @@ var KimiDriver = class {
2773
2935
  return detectKimiModels();
2774
2936
  }
2775
2937
  };
2776
- function detectKimiModels(home = os2.homedir()) {
2938
+ function detectKimiModels(home = os3.homedir()) {
2777
2939
  const configPath = path8.join(home, ".kimi", "config.toml");
2778
2940
  let raw;
2779
2941
  try {
2780
- raw = readFileSync2(configPath, "utf8");
2942
+ raw = readFileSync3(configPath, "utf8");
2781
2943
  } catch {
2782
2944
  return null;
2783
2945
  }
@@ -2800,9 +2962,9 @@ function detectKimiModels(home = os2.homedir()) {
2800
2962
  }
2801
2963
 
2802
2964
  // src/drivers/opencode.ts
2803
- import { spawn as spawn7 } from "child_process";
2804
- import { readFileSync as readFileSync3 } from "fs";
2805
- import os3 from "os";
2965
+ import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
2966
+ import { readFileSync as readFileSync4 } from "fs";
2967
+ import os4 from "os";
2806
2968
  import path9 from "path";
2807
2969
  var CHAT_MCP_SERVER_NAME = "chat";
2808
2970
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
@@ -2842,10 +3004,10 @@ function parseUserOpenCodeConfig(ctx) {
2842
3004
  const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2843
3005
  return parseOpenCodeConfigContent(raw);
2844
3006
  }
2845
- function readLocalOpenCodeConfig(home = os3.homedir()) {
3007
+ function readLocalOpenCodeConfig(home = os4.homedir()) {
2846
3008
  const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2847
3009
  try {
2848
- return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
3010
+ return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
2849
3011
  } catch {
2850
3012
  }
2851
3013
  return {};
@@ -2891,7 +3053,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
2891
3053
  }
2892
3054
  };
2893
3055
  }
2894
- function buildOpenCodeConfig(ctx, home = os3.homedir()) {
3056
+ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
2895
3057
  const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2896
3058
  const userAgents = recordField(userConfig.agent);
2897
3059
  const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
@@ -2916,7 +3078,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2916
3078
  }
2917
3079
  };
2918
3080
  }
2919
- function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
3081
+ function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
2920
3082
  const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2921
3083
  const config = buildOpenCodeConfig(ctx, home);
2922
3084
  const env = {
@@ -2943,24 +3105,42 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2943
3105
  args.push("--", turnPrompt);
2944
3106
  return { args, env, config };
2945
3107
  }
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
- }
3108
+ function parseOpenCodeModelsOutput(output) {
3109
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
3110
+ const models = [];
3111
+ const seen = /* @__PURE__ */ new Set();
3112
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
3113
+ const line = rawLine.trim();
3114
+ if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
3115
+ if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
3116
+ if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
3117
+ if (seen.has(line)) continue;
3118
+ seen.add(line);
3119
+ models.push({
3120
+ id: line,
3121
+ label: line,
3122
+ verified: "launchable"
3123
+ });
2961
3124
  }
2962
3125
  return models.length > 0 ? { models } : null;
2963
3126
  }
3127
+ function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3128
+ const commandResult = runCommand(home);
3129
+ if (commandResult.error || commandResult.status !== 0) return null;
3130
+ return parseOpenCodeModelsOutput(commandResult.stdout);
3131
+ }
3132
+ function runOpenCodeModelsCommand(home) {
3133
+ const result = spawnSync2("opencode", ["models"], {
3134
+ env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
3135
+ encoding: "utf8",
3136
+ timeout: 5e3
3137
+ });
3138
+ return {
3139
+ status: result.status,
3140
+ stdout: String(result.stdout || ""),
3141
+ error: result.error
3142
+ };
3143
+ }
2964
3144
  function isSystemFirstMessageTask(message) {
2965
3145
  return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2966
3146
  }
@@ -3236,9 +3416,39 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3236
3416
  }
3237
3417
 
3238
3418
  // src/agentProcessManager.ts
3239
- var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
3240
- var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 1;
3419
+ var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3420
+ var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3241
3421
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3422
+ var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
3423
+ var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
3424
+ var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
3425
+ ".md",
3426
+ ".txt",
3427
+ ".json",
3428
+ ".js",
3429
+ ".ts",
3430
+ ".jsx",
3431
+ ".tsx",
3432
+ ".yaml",
3433
+ ".yml",
3434
+ ".toml",
3435
+ ".log",
3436
+ ".csv",
3437
+ ".xml",
3438
+ ".html",
3439
+ ".css",
3440
+ ".sh",
3441
+ ".py"
3442
+ ]);
3443
+ var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
3444
+ ".apng": "image/apng",
3445
+ ".avif": "image/avif",
3446
+ ".gif": "image/gif",
3447
+ ".jpg": "image/jpeg",
3448
+ ".jpeg": "image/jpeg",
3449
+ ".png": "image/png",
3450
+ ".webp": "image/webp"
3451
+ };
3242
3452
  function readPositiveIntegerEnv(name, fallback) {
3243
3453
  const raw = process.env[name];
3244
3454
  if (!raw) return fallback;
@@ -3278,6 +3488,7 @@ function formatMessageTarget(message) {
3278
3488
  function getMessageShortId(messageId) {
3279
3489
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
3280
3490
  }
3491
+ 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
3492
  function findSessionJsonl(root, predicate) {
3282
3493
  let visited = 0;
3283
3494
  const maxEntries = 1e4;
@@ -3286,7 +3497,7 @@ function findSessionJsonl(root, predicate) {
3286
3497
  if (depth < 0 || visited >= maxEntries) return null;
3287
3498
  let entries;
3288
3499
  try {
3289
- entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3500
+ entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3290
3501
  } catch {
3291
3502
  return null;
3292
3503
  }
@@ -3332,11 +3543,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3332
3543
  return null;
3333
3544
  }
3334
3545
  }
3335
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3546
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3336
3547
  const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3337
3548
  if (directPath) {
3338
3549
  try {
3339
- if (statSync(directPath).isFile()) {
3550
+ if (statSync2(directPath).isFile()) {
3340
3551
  return { label: sessionId, path: directPath, runtime, reachable: true };
3341
3552
  }
3342
3553
  } catch {
@@ -3432,6 +3643,16 @@ function formatRuntimeProfileControlPrompt(messages) {
3432
3643
  return null;
3433
3644
  }
3434
3645
  const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
3646
+ const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
3647
+ if (!hasMigration) {
3648
+ return [
3649
+ "Runtime Profile daemon release notice.",
3650
+ "",
3651
+ "Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
3652
+ "",
3653
+ body
3654
+ ].join("\n");
3655
+ }
3435
3656
  return [
3436
3657
  "Runtime Profile control notice.",
3437
3658
  "",
@@ -3937,6 +4158,15 @@ function classifyTerminalFailure(ap) {
3937
4158
  }
3938
4159
  return null;
3939
4160
  }
4161
+ function hasDirectStdinRecoveryEvidence(ap) {
4162
+ const candidates = [
4163
+ ap.lastRuntimeError,
4164
+ ...ap.recentStderr
4165
+ ].filter((value) => !!value);
4166
+ return candidates.some(
4167
+ (text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
4168
+ );
4169
+ }
3940
4170
  function isMissingResumeSession(ap) {
3941
4171
  if (!ap.sessionId) return false;
3942
4172
  const candidates = [
@@ -3978,7 +4208,6 @@ var AgentProcessManager = class _AgentProcessManager {
3978
4208
  // Prevent concurrent starts of same agent
3979
4209
  queuedAgentStarts = /* @__PURE__ */ new Map();
3980
4210
  agentStartQueue = [];
3981
- activeAgentStartPermits = /* @__PURE__ */ new Set();
3982
4211
  activeAgentStartCount = 0;
3983
4212
  agentStartPumpTimer = null;
3984
4213
  lastAgentStartAt = 0;
@@ -3998,6 +4227,7 @@ var AgentProcessManager = class _AgentProcessManager {
3998
4227
  driverResolver;
3999
4228
  defaultAgentEnvVarsProvider;
4000
4229
  tracer;
4230
+ deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
4001
4231
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
4002
4232
  this.chatBridgePath = chatBridgePath;
4003
4233
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -4005,7 +4235,7 @@ var AgentProcessManager = class _AgentProcessManager {
4005
4235
  this.daemonApiKey = daemonApiKey;
4006
4236
  this.serverUrl = opts.serverUrl;
4007
4237
  this.dataDir = opts.dataDir || DATA_DIR;
4008
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
4238
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
4009
4239
  this.driverResolver = opts.driverResolver || getDriver;
4010
4240
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
4011
4241
  this.tracer = opts.tracer ?? noopTracer;
@@ -4022,16 +4252,75 @@ var AgentProcessManager = class _AgentProcessManager {
4022
4252
  )
4023
4253
  );
4024
4254
  }
4255
+ setTracer(tracer) {
4256
+ this.tracer = tracer;
4257
+ }
4258
+ recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
4259
+ const span = this.tracer.startSpan(name, {
4260
+ parent: parseTraceparent(parentTraceparent),
4261
+ surface: "daemon",
4262
+ kind: "internal",
4263
+ attrs
4264
+ });
4265
+ span.end(status);
4266
+ }
4267
+ startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4268
+ return {
4269
+ agentId,
4270
+ launchId,
4271
+ runtime: config.runtime,
4272
+ model: config.model,
4273
+ session_id_present: Boolean(config.sessionId),
4274
+ launch_id_present: Boolean(launchId),
4275
+ wake_message_present: Boolean(wakeMessage),
4276
+ unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
4277
+ resume_prompt_present: Boolean(resumePrompt),
4278
+ queue_depth: this.agentStartQueue.length,
4279
+ active_starts: this.activeAgentStartCount,
4280
+ max_concurrent_starts: this.maxConcurrentAgentStarts,
4281
+ min_start_interval_ms: this.agentStartIntervalMs
4282
+ };
4283
+ }
4284
+ getDeliveryTraceContext(message) {
4285
+ return this.deliveryTraceContexts.get(message) ?? {};
4286
+ }
4287
+ deliveryTraceAttrs(agentId, message, attrs = {}) {
4288
+ const context = this.getDeliveryTraceContext(message);
4289
+ const deliveryCorrelationId = context.deliveryId ?? message.message_id;
4290
+ return {
4291
+ agentId,
4292
+ deliveryId: context.deliveryId,
4293
+ delivery_correlation_id: deliveryCorrelationId,
4294
+ channel_type: message.channel_type,
4295
+ sender_type: message.sender_type,
4296
+ messageId: message.message_id,
4297
+ message_id_present: Boolean(message.message_id),
4298
+ ...attrs
4299
+ };
4300
+ }
4025
4301
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4302
+ this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4026
4303
  if (this.agents.has(agentId)) {
4304
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4305
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4306
+ reason: "already_running"
4307
+ });
4027
4308
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4028
4309
  return;
4029
4310
  }
4030
4311
  if (this.agentsStarting.has(agentId)) {
4312
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4313
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4314
+ reason: "already_starting"
4315
+ });
4031
4316
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4032
4317
  return;
4033
4318
  }
4034
4319
  if (this.queuedAgentStarts.has(agentId)) {
4320
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4321
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4322
+ reason: "already_queued"
4323
+ });
4035
4324
  logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
4036
4325
  return;
4037
4326
  }
@@ -4048,6 +4337,7 @@ var AgentProcessManager = class _AgentProcessManager {
4048
4337
  };
4049
4338
  this.agentStartQueue.push(item);
4050
4339
  this.queuedAgentStarts.set(agentId, item);
4340
+ this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4051
4341
  logger.info(
4052
4342
  `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
4053
4343
  );
@@ -4063,6 +4353,10 @@ var AgentProcessManager = class _AgentProcessManager {
4063
4353
  const elapsed = Date.now() - this.lastAgentStartAt;
4064
4354
  const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
4065
4355
  if (waitMs > 0) {
4356
+ this.recordDaemonTrace("daemon.agent.start.rate_limited", {
4357
+ ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
4358
+ wait_ms: waitMs
4359
+ });
4066
4360
  this.agentStartPumpTimer = setTimeout(() => {
4067
4361
  this.agentStartPumpTimer = null;
4068
4362
  this.pumpAgentStartQueue();
@@ -4072,23 +4366,31 @@ var AgentProcessManager = class _AgentProcessManager {
4072
4366
  const item = this.agentStartQueue.shift();
4073
4367
  if (!item) return;
4074
4368
  if (this.queuedAgentStarts.get(item.agentId) !== item) {
4369
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4370
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4371
+ reason: "stale_queue_item"
4372
+ });
4075
4373
  this.pumpAgentStartQueue();
4076
4374
  return;
4077
4375
  }
4078
4376
  this.queuedAgentStarts.delete(item.agentId);
4079
4377
  if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
4378
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4379
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4380
+ reason: "already_running_or_starting"
4381
+ });
4080
4382
  logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
4081
4383
  item.resolve();
4082
4384
  this.pumpAgentStartQueue();
4083
4385
  return;
4084
4386
  }
4085
4387
  this.activeAgentStartCount++;
4086
- this.activeAgentStartPermits.add(item.agentId);
4087
4388
  this.lastAgentStartAt = Date.now();
4088
4389
  this.lastAgentStartAgentId = item.agentId;
4089
4390
  logger.info(
4090
4391
  `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
4091
4392
  );
4393
+ this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
4092
4394
  this.startAgentNow(
4093
4395
  item.agentId,
4094
4396
  item.config,
@@ -4096,19 +4398,28 @@ var AgentProcessManager = class _AgentProcessManager {
4096
4398
  item.unreadSummary,
4097
4399
  item.resumePrompt,
4098
4400
  item.launchId
4099
- ).then(item.resolve, (err) => {
4100
- this.releaseAgentStartPermit(item.agentId, "start failed");
4401
+ ).then(() => {
4402
+ this.releaseAgentStartSlot(item.agentId, "spawn attempted");
4403
+ item.resolve();
4404
+ }, (err) => {
4405
+ this.releaseAgentStartSlot(item.agentId, "start failed");
4101
4406
  item.reject(err);
4102
4407
  });
4103
4408
  }
4104
- releaseAgentStartPermit(agentId, reason) {
4105
- if (!this.activeAgentStartPermits.delete(agentId)) return false;
4409
+ releaseAgentStartSlot(agentId, reason) {
4410
+ if (this.activeAgentStartCount <= 0) return;
4106
4411
  this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
4412
+ this.recordDaemonTrace("daemon.agent.start.slot_released", {
4413
+ agentId,
4414
+ reason,
4415
+ active_starts: this.activeAgentStartCount,
4416
+ queue_depth: this.agentStartQueue.length,
4417
+ max_concurrent_starts: this.maxConcurrentAgentStarts
4418
+ });
4107
4419
  logger.info(
4108
- `[Agent ${agentId}] Start permit released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4420
+ `[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4109
4421
  );
4110
4422
  this.pumpAgentStartQueue();
4111
- return true;
4112
4423
  }
4113
4424
  cancelQueuedAgentStart(agentId, reason) {
4114
4425
  const item = this.queuedAgentStarts.get(agentId);
@@ -4120,6 +4431,10 @@ var AgentProcessManager = class _AgentProcessManager {
4120
4431
  clearTimeout(this.agentStartPumpTimer);
4121
4432
  this.agentStartPumpTimer = null;
4122
4433
  }
4434
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4435
+ ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4436
+ reason
4437
+ }, "cancelled");
4123
4438
  logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
4124
4439
  item.resolve();
4125
4440
  return true;
@@ -4127,6 +4442,10 @@ var AgentProcessManager = class _AgentProcessManager {
4127
4442
  cancelAllQueuedAgentStarts(reason) {
4128
4443
  for (const item of this.agentStartQueue) {
4129
4444
  if (this.queuedAgentStarts.get(item.agentId) === item) {
4445
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4446
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4447
+ reason
4448
+ }, "cancelled");
4130
4449
  logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
4131
4450
  item.resolve();
4132
4451
  }
@@ -4141,14 +4460,23 @@ var AgentProcessManager = class _AgentProcessManager {
4141
4460
  }
4142
4461
  async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4143
4462
  if (this.agents.has(agentId)) {
4463
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4464
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4465
+ reason: "already_running"
4466
+ });
4144
4467
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4145
4468
  return;
4146
4469
  }
4147
4470
  if (this.agentsStarting.has(agentId)) {
4471
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4472
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4473
+ reason: "already_starting"
4474
+ });
4148
4475
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4149
4476
  return;
4150
4477
  }
4151
4478
  this.agentsStarting.add(agentId);
4479
+ this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4152
4480
  try {
4153
4481
  const driver = this.driverResolver(config.runtime || "claude");
4154
4482
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4208,6 +4536,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
4208
4536
  prompt += `
4209
4537
 
4210
4538
  Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
4539
+ ${RESPONSE_TARGET_HINT}
4211
4540
 
4212
4541
  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
4542
  prompt += getBusyDeliveryNote(driver);
@@ -4240,8 +4569,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4240
4569
  });
4241
4570
  this.sendAgentStatus(agentId, "active", launchId || null);
4242
4571
  this.broadcastActivity(agentId, "online", "Process idle");
4572
+ this.recordDaemonTrace("daemon.agent.spawn.deferred", {
4573
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4574
+ pending_messages_count: pendingMessages.length,
4575
+ reason: "defer_until_concrete_message"
4576
+ });
4243
4577
  logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
4244
- this.releaseAgentStartPermit(agentId, "spawn deferred");
4245
4578
  for (const message of pendingMessages) {
4246
4579
  this.deliverMessage(agentId, message);
4247
4580
  }
@@ -4258,6 +4591,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4258
4591
  daemonApiKey: this.daemonApiKey,
4259
4592
  launchId: launchId || null
4260
4593
  });
4594
+ this.recordDaemonTrace("daemon.agent.spawn.created", {
4595
+ ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
4596
+ detached: false,
4597
+ new_session: false,
4598
+ process_pid_present: typeof proc.pid === "number"
4599
+ });
4261
4600
  const agentProcess = {
4262
4601
  process: proc,
4263
4602
  driver,
@@ -4292,7 +4631,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4292
4631
  };
4293
4632
  this.startingInboxes.delete(agentId);
4294
4633
  this.agents.set(agentId, agentProcess);
4295
- this.startRuntimeTrace(agentId, agentProcess, "spawn");
4634
+ this.idleAgentConfigs.set(agentId, {
4635
+ config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
4636
+ sessionId: effectiveConfig.sessionId || null,
4637
+ launchId: launchId || null
4638
+ });
4639
+ this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
4296
4640
  this.agentsStarting.delete(agentId);
4297
4641
  if (config.runtimeProfileControl) {
4298
4642
  this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
@@ -4331,6 +4675,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4331
4675
  proc.on("error", (err) => {
4332
4676
  const current = this.agents.get(agentId);
4333
4677
  if (current) current.spawnError = err.message;
4678
+ this.recordDaemonTrace("daemon.agent.process.error", {
4679
+ agentId,
4680
+ launchId: current?.launchId || void 0,
4681
+ runtime: config.runtime,
4682
+ model: config.model,
4683
+ error_class: err.name || typeof err
4684
+ }, "error");
4334
4685
  logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
4335
4686
  });
4336
4687
  proc.on("exit", (code, signal) => {
@@ -4339,13 +4690,24 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4339
4690
  current.exitCode = code;
4340
4691
  current.exitSignal = signal;
4341
4692
  }
4693
+ this.recordDaemonTrace("daemon.agent.process.exited", {
4694
+ agentId,
4695
+ launchId: current?.launchId || void 0,
4696
+ runtime: config.runtime,
4697
+ model: config.model,
4698
+ exit_code: code,
4699
+ exit_signal: signal,
4700
+ clean_exit: code === 0,
4701
+ runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4702
+ inbox_count: current?.inbox.length ?? 0,
4703
+ pending_notification_count: current?.pendingNotificationCount ?? 0
4704
+ });
4342
4705
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4343
4706
  });
4344
4707
  proc.on("close", (code, signal) => {
4345
4708
  if (this.agents.has(agentId)) {
4346
4709
  const ap = this.agents.get(agentId);
4347
4710
  if (ap.process !== proc) return;
4348
- this.releaseAgentStartPermit(agentId, "process closed");
4349
4711
  if (ap.notificationTimer) {
4350
4712
  clearTimeout(ap.notificationTimer);
4351
4713
  }
@@ -4403,7 +4765,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4403
4765
  }
4404
4766
  if (processEndedCleanly) {
4405
4767
  let queuedWakeMessage;
4406
- if (!ap.driver.supportsStdinNotification) {
4768
+ if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
4407
4769
  while (ap.inbox.length > 0) {
4408
4770
  const candidate = ap.inbox.shift();
4409
4771
  if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
@@ -4509,6 +4871,66 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4509
4871
  }
4510
4872
  return leftKeys.every((key) => left?.[key] === right?.[key]);
4511
4873
  }
4874
+ enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
4875
+ ap.inbox.push(message);
4876
+ if (ap.driver.supportsStdinNotification && ap.sessionId) {
4877
+ ap.pendingNotificationCount++;
4878
+ if (ap.driver.busyDeliveryMode === "gated") {
4879
+ this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4880
+ reason: "runtime_profile",
4881
+ kind,
4882
+ pendingMessages: ap.inbox.length
4883
+ });
4884
+ } else if (!ap.notificationTimer) {
4885
+ ap.notificationTimer = setTimeout(() => {
4886
+ this.sendStdinNotification(agentId);
4887
+ }, 3e3);
4888
+ }
4889
+ }
4890
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
4891
+ agentId,
4892
+ kind,
4893
+ key_present: Boolean(key),
4894
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
4895
+ runtime: ap.config.runtime,
4896
+ session_id_present: Boolean(ap.sessionId),
4897
+ launchId: ap.launchId || void 0,
4898
+ inbox_count: ap.inbox.length,
4899
+ pending_notification_count: ap.pendingNotificationCount,
4900
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
4901
+ supports_stdin_notification: ap.driver.supportsStdinNotification
4902
+ });
4903
+ logger.info(
4904
+ `[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
4905
+ );
4906
+ }
4907
+ queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
4908
+ const pending = this.startingInboxes.get(agentId) || [];
4909
+ pending.push(message);
4910
+ this.startingInboxes.set(agentId, pending);
4911
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4912
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
4913
+ agentId,
4914
+ kind,
4915
+ key_present: Boolean(key),
4916
+ outcome: "queued_during_start",
4917
+ startup_pending: true,
4918
+ starting_inbox_count: pending.length,
4919
+ launchId: queuedStart?.launchId
4920
+ });
4921
+ logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
4922
+ }
4923
+ splitRuntimeProfileControlBatch(messages) {
4924
+ const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
4925
+ if (controlMessages.length === 0 || controlMessages.length === messages.length) {
4926
+ return { nextMessages: messages, deferredMessages: [] };
4927
+ }
4928
+ const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
4929
+ return { nextMessages: controlMessages, deferredMessages };
4930
+ }
4931
+ containsOrdinaryInboxMessage(messages) {
4932
+ return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
4933
+ }
4512
4934
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
4513
4935
  this.cancelQueuedAgentStart(agentId, "stop requested");
4514
4936
  this.idleAgentConfigs.delete(agentId);
@@ -4519,7 +4941,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4519
4941
  }
4520
4942
  return;
4521
4943
  }
4522
- this.releaseAgentStartPermit(agentId, "stop requested");
4523
4944
  if (ap.notificationTimer) {
4524
4945
  clearTimeout(ap.notificationTimer);
4525
4946
  }
@@ -4558,51 +4979,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4558
4979
  });
4559
4980
  }
4560
4981
  }
4561
- deliverMessage(agentId, message) {
4982
+ deliverMessage(agentId, message, traceContext = {}) {
4983
+ if (traceContext.deliveryId) {
4984
+ this.deliveryTraceContexts.set(message, traceContext);
4985
+ }
4562
4986
  const ap = this.agents.get(agentId);
4563
4987
  if (!ap) {
4564
4988
  if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
4989
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4565
4990
  const pending = this.startingInboxes.get(agentId) || [];
4566
4991
  pending.push(message);
4567
4992
  this.startingInboxes.set(agentId, pending);
4568
- return;
4993
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
4994
+ outcome: "queued_during_start",
4995
+ accepted: true,
4996
+ process_present: false,
4997
+ startup_pending: true,
4998
+ starting_inbox_count: pending.length,
4999
+ launchId: queuedStart?.launchId
5000
+ }));
5001
+ return true;
4569
5002
  }
4570
5003
  const cached = this.idleAgentConfigs.get(agentId);
4571
5004
  if (cached) {
4572
5005
  const driver = this.driverResolver(cached.config.runtime || "claude");
4573
5006
  if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4574
- return;
5007
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5008
+ outcome: "deferred_wake_message",
5009
+ accepted: true,
5010
+ process_present: false,
5011
+ cached_idle_config_present: true,
5012
+ runtime: cached.config.runtime,
5013
+ session_id_present: Boolean(cached.sessionId),
5014
+ launchId: cached.launchId || void 0
5015
+ }));
5016
+ return true;
4575
5017
  }
4576
5018
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
4577
5019
  this.idleAgentConfigs.delete(agentId);
5020
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5021
+ outcome: "auto_restart_from_idle",
5022
+ accepted: true,
5023
+ process_present: false,
5024
+ cached_idle_config_present: true,
5025
+ runtime: cached.config.runtime,
5026
+ session_id_present: Boolean(cached.sessionId),
5027
+ launchId: cached.launchId || void 0
5028
+ }));
4578
5029
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
4579
5030
  logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
4580
5031
  });
5032
+ return true;
4581
5033
  }
4582
- return;
5034
+ logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
5035
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5036
+ outcome: "rejected_no_process",
5037
+ accepted: false,
5038
+ process_present: false,
5039
+ cached_idle_config_present: false
5040
+ }), "error");
5041
+ this.sendAgentStatus(agentId, "inactive", null);
5042
+ this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
5043
+ return false;
4583
5044
  }
4584
5045
  if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4585
- return;
5046
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5047
+ outcome: "deferred_wake_message",
5048
+ accepted: true,
5049
+ process_present: true,
5050
+ runtime: ap.config.runtime,
5051
+ session_id_present: Boolean(ap.sessionId),
5052
+ launchId: ap.launchId || void 0,
5053
+ is_idle: ap.isIdle,
5054
+ inbox_count: ap.inbox.length
5055
+ }));
5056
+ return true;
4586
5057
  }
4587
5058
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
4588
5059
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4589
5060
  nextMessages.push(message);
4590
5061
  ap.isIdle = false;
4591
- this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
5062
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
4592
5063
  this.broadcastActivity(agentId, "working", "Message received");
4593
- this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
4594
- return;
5064
+ const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
5065
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5066
+ outcome: "stdin_idle_delivery",
5067
+ accepted: true,
5068
+ process_present: true,
5069
+ runtime: ap.config.runtime,
5070
+ session_id_present: true,
5071
+ launchId: ap.launchId || void 0,
5072
+ stdin_delivery_accepted: stdinAccepted,
5073
+ delivered_messages_count: nextMessages.length
5074
+ }));
5075
+ return true;
4595
5076
  }
4596
5077
  ap.inbox.push(message);
4597
- if (!ap.driver.supportsStdinNotification) return;
4598
- if (!ap.sessionId) return;
5078
+ if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
5079
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5080
+ outcome: "queued_stalled_recovery",
5081
+ accepted: true,
5082
+ process_present: true,
5083
+ runtime: ap.config.runtime,
5084
+ session_id_present: Boolean(ap.sessionId),
5085
+ launchId: ap.launchId || void 0,
5086
+ inbox_count: ap.inbox.length
5087
+ }));
5088
+ return true;
5089
+ }
5090
+ if (!ap.driver.supportsStdinNotification) {
5091
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5092
+ outcome: "queued_busy_non_stdin",
5093
+ accepted: true,
5094
+ process_present: true,
5095
+ runtime: ap.config.runtime,
5096
+ session_id_present: Boolean(ap.sessionId),
5097
+ launchId: ap.launchId || void 0,
5098
+ inbox_count: ap.inbox.length
5099
+ }));
5100
+ return true;
5101
+ }
5102
+ if (!ap.sessionId) {
5103
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5104
+ outcome: "queued_before_session",
5105
+ accepted: true,
5106
+ process_present: true,
5107
+ runtime: ap.config.runtime,
5108
+ session_id_present: false,
5109
+ launchId: ap.launchId || void 0,
5110
+ inbox_count: ap.inbox.length
5111
+ }));
5112
+ return true;
5113
+ }
4599
5114
  if (ap.driver.busyDeliveryMode === "gated") {
4600
5115
  ap.pendingNotificationCount++;
4601
5116
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4602
5117
  reason: "busy_message",
4603
5118
  pendingMessages: ap.inbox.length
4604
5119
  });
4605
- return;
5120
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5121
+ outcome: "queued_busy_gated",
5122
+ accepted: true,
5123
+ process_present: true,
5124
+ runtime: ap.config.runtime,
5125
+ session_id_present: true,
5126
+ launchId: ap.launchId || void 0,
5127
+ inbox_count: ap.inbox.length,
5128
+ pending_notification_count: ap.pendingNotificationCount
5129
+ }));
5130
+ return true;
4606
5131
  }
4607
5132
  ap.pendingNotificationCount++;
4608
5133
  if (!ap.notificationTimer) {
@@ -4610,6 +5135,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4610
5135
  this.sendStdinNotification(agentId);
4611
5136
  }, 3e3);
4612
5137
  }
5138
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5139
+ outcome: "queued_busy_notification",
5140
+ accepted: true,
5141
+ process_present: true,
5142
+ runtime: ap.config.runtime,
5143
+ session_id_present: true,
5144
+ inbox_count: ap.inbox.length,
5145
+ pending_notification_count: ap.pendingNotificationCount,
5146
+ notification_timer_present: Boolean(ap.notificationTimer)
5147
+ }));
5148
+ return true;
4613
5149
  }
4614
5150
  async resetWorkspace(agentId) {
4615
5151
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4643,6 +5179,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4643
5179
  getIdleAgentSessionIds() {
4644
5180
  const result = [];
4645
5181
  for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
5182
+ if (this.agents.has(agentId)) continue;
4646
5183
  if (sessionId) result.push({ agentId, sessionId, launchId });
4647
5184
  }
4648
5185
  return result;
@@ -4694,7 +5231,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4694
5231
  }
4695
5232
  return reports;
4696
5233
  }
4697
- deliverRuntimeProfileNotification(agentId, key, kind, content) {
5234
+ deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
5235
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5236
+ parent: parseTraceparent(traceparent),
5237
+ surface: "daemon",
5238
+ kind: "consumer",
5239
+ attrs: {
5240
+ agentId,
5241
+ control_kind: kind,
5242
+ key_present: Boolean(key)
5243
+ }
5244
+ });
4698
5245
  const now = (/* @__PURE__ */ new Date()).toISOString();
4699
5246
  const message = {
4700
5247
  channel_id: "system",
@@ -4705,18 +5252,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4705
5252
  sender_type: "system",
4706
5253
  content,
4707
5254
  timestamp: now,
4708
- message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
5255
+ message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
5256
+ traceparent: formatTraceparent(span.context)
4709
5257
  };
4710
5258
  const ap = this.agents.get(agentId);
5259
+ if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
5260
+ this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
5261
+ span.end("ok", {
5262
+ attrs: {
5263
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5264
+ runtime: ap.config.runtime,
5265
+ launchId: ap.launchId || void 0,
5266
+ session_id_present: Boolean(ap.sessionId),
5267
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5268
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5269
+ }
5270
+ });
5271
+ return true;
5272
+ }
4711
5273
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
4712
5274
  ap.isIdle = false;
4713
5275
  this.startRuntimeTrace(agentId, ap, "runtime-profile");
4714
- this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
4715
- return;
5276
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5277
+ span.end(written ? "ok" : "error", {
5278
+ attrs: {
5279
+ outcome: written ? "stdin_idle" : "stdin_failed",
5280
+ runtime: ap.config.runtime,
5281
+ launchId: ap.launchId || void 0,
5282
+ session_id_present: true,
5283
+ supports_stdin_notification: true,
5284
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5285
+ }
5286
+ });
5287
+ return written;
4716
5288
  }
4717
5289
  if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
4718
- this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
4719
- return;
5290
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
5291
+ span.end(written ? "ok" : "error", {
5292
+ attrs: {
5293
+ outcome: written ? "stdin_busy" : "stdin_failed",
5294
+ runtime: ap.config.runtime,
5295
+ launchId: ap.launchId || void 0,
5296
+ session_id_present: true,
5297
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5298
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5299
+ }
5300
+ });
5301
+ return written;
5302
+ }
5303
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5304
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5305
+ this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
5306
+ span.end("ok", {
5307
+ attrs: {
5308
+ outcome: "queued_during_start",
5309
+ startup_pending: true,
5310
+ launchId: queuedStart?.launchId
5311
+ }
5312
+ });
5313
+ return true;
4720
5314
  }
4721
5315
  const cached = this.idleAgentConfigs.get(agentId);
4722
5316
  if (cached) {
@@ -4726,9 +5320,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4726
5320
  logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
4727
5321
  this.idleAgentConfigs.set(agentId, cached);
4728
5322
  });
4729
- return;
5323
+ span.end("ok", {
5324
+ attrs: {
5325
+ outcome: "restart_queued",
5326
+ runtime: cached.config.runtime,
5327
+ launchId: cached.launchId || void 0,
5328
+ session_id_present: Boolean(cached.sessionId)
5329
+ }
5330
+ });
5331
+ return true;
4730
5332
  }
4731
5333
  logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
5334
+ span.end("ok", { attrs: { outcome: "no_path" } });
5335
+ return false;
4732
5336
  }
4733
5337
  ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
4734
5338
  for (const message of messages) {
@@ -4741,19 +5345,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4741
5345
  type: "agent:runtime_profile:migration:ack",
4742
5346
  agentId,
4743
5347
  migrationKey: notification.key,
4744
- launchId: launchId || void 0
5348
+ launchId: launchId || void 0,
5349
+ traceparent: message.traceparent
4745
5350
  });
4746
5351
  } else {
4747
5352
  this.sendToServer({
4748
5353
  type: "agent:runtime_profile:daemon_release_notice:ack",
4749
5354
  agentId,
4750
5355
  noticeKey: notification.key,
4751
- launchId: launchId || void 0
5356
+ launchId: launchId || void 0,
5357
+ traceparent: message.traceparent
4752
5358
  });
4753
5359
  }
4754
5360
  }
4755
5361
  }
4756
5362
  ackInjectedRuntimeProfileControl(agentId, control, launchId) {
5363
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5364
+ surface: "daemon",
5365
+ kind: "internal",
5366
+ attrs: {
5367
+ agentId,
5368
+ control_kind: control.kind,
5369
+ key_present: Boolean(control.key),
5370
+ launchId: launchId || void 0,
5371
+ source: "agent_config"
5372
+ }
5373
+ });
4757
5374
  const title = runtimeProfileNotificationTitle(control.kind);
4758
5375
  this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
4759
5376
  if (control.kind === "migration") {
@@ -4761,24 +5378,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4761
5378
  type: "agent:runtime_profile:migration:ack",
4762
5379
  agentId,
4763
5380
  migrationKey: control.key,
4764
- launchId: launchId || void 0
5381
+ launchId: launchId || void 0,
5382
+ traceparent: formatTraceparent(span.context)
4765
5383
  });
4766
5384
  } else {
4767
5385
  this.sendToServer({
4768
5386
  type: "agent:runtime_profile:daemon_release_notice:ack",
4769
5387
  agentId,
4770
5388
  noticeKey: control.key,
4771
- launchId: launchId || void 0
5389
+ launchId: launchId || void 0,
5390
+ traceparent: formatTraceparent(span.context)
4772
5391
  });
4773
5392
  }
5393
+ span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
4774
5394
  }
4775
5395
  sendRuntimeProfileWireReport(report) {
5396
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
5397
+ surface: "daemon",
5398
+ kind: "producer",
5399
+ attrs: {
5400
+ agentId: report.agentId,
5401
+ launchId: report.launchId || void 0,
5402
+ runtime: report.facts.runtime,
5403
+ model_present: Boolean(report.facts.model),
5404
+ session_ref_present: Boolean(report.facts.sessionRef),
5405
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
5406
+ }
5407
+ });
4776
5408
  this.sendToServer({
4777
5409
  type: "agent:runtime_profile",
4778
5410
  agentId: report.agentId,
4779
5411
  facts: report.facts,
4780
- launchId: report.launchId || void 0
5412
+ launchId: report.launchId || void 0,
5413
+ traceparent: formatTraceparent(span.context)
4781
5414
  });
5415
+ span.end("ok");
4782
5416
  }
4783
5417
  sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4784
5418
  this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
@@ -4821,32 +5455,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4821
5455
  }
4822
5456
  const info = await stat2(resolved);
4823
5457
  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
5458
  const ext = path11.extname(resolved).toLowerCase();
4844
- if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4845
- return { content: null, binary: true };
5459
+ if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5460
+ if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5461
+ const content = await readFile(resolved, "utf-8");
5462
+ return { content, binary: false, size: info.size, encoding: "utf-8" };
5463
+ }
5464
+ const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
5465
+ if (imageMimeType) {
5466
+ if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
5467
+ return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
5468
+ }
5469
+ const content = await readFile(resolved, "base64");
5470
+ return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
4846
5471
  }
4847
- if (info.size > 1048576) throw new Error("File too large");
4848
- const content = await readFile(resolved, "utf-8");
4849
- return { content, binary: false };
5472
+ return { content: null, binary: true, size: info.size };
4850
5473
  }
4851
5474
  // Skill scanning
4852
5475
  // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
@@ -4866,7 +5489,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4866
5489
  async listSkills(agentId, runtimeHint) {
4867
5490
  const agent = this.agents.get(agentId);
4868
5491
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4869
- const home = os4.homedir();
5492
+ const home = os5.homedir();
4870
5493
  const workspaceDir = path11.join(this.dataDir, agentId);
4871
5494
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4872
5495
  const globalResults = await Promise.all(
@@ -5096,7 +5719,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5096
5719
  this.clearCompactionWatchdog(ap);
5097
5720
  this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
5098
5721
  }
5099
- startRuntimeTrace(agentId, ap, reason) {
5722
+ messagesTraceAttrs(messages) {
5723
+ if (!messages || messages.length === 0) return {};
5724
+ const first = messages[0];
5725
+ const context = this.getDeliveryTraceContext(first);
5726
+ const notification = runtimeProfileNotificationFromMessage(first);
5727
+ if (notification) {
5728
+ return {
5729
+ messages_count: messages.length,
5730
+ message_id_present: Boolean(first.message_id),
5731
+ deliveryId: context.deliveryId,
5732
+ delivery_correlation_id: context.deliveryId,
5733
+ control_kind: notification.kind,
5734
+ key_present: Boolean(notification.key)
5735
+ };
5736
+ }
5737
+ return {
5738
+ messages_count: messages.length,
5739
+ messageId: first.message_id,
5740
+ message_id_present: Boolean(first.message_id),
5741
+ deliveryId: context.deliveryId,
5742
+ delivery_correlation_id: context.deliveryId ?? first.message_id
5743
+ };
5744
+ }
5745
+ startRuntimeTrace(agentId, ap, reason, messages) {
5100
5746
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
5101
5747
  const span = this.tracer.startSpan("daemon.runtime.turn", {
5102
5748
  surface: "daemon",
@@ -5106,10 +5752,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5106
5752
  runtime: ap.config.runtime,
5107
5753
  model: ap.config.model,
5108
5754
  reason,
5109
- hasSession: Boolean(ap.sessionId)
5755
+ hasSession: Boolean(ap.sessionId),
5756
+ ...this.messagesTraceAttrs(messages)
5110
5757
  }
5111
5758
  });
5112
- span.addEvent("daemon.turn.started", { reason });
5759
+ span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
5113
5760
  ap.runtimeTraceSpan = span;
5114
5761
  return span;
5115
5762
  }
@@ -5221,6 +5868,48 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5221
5868
  this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
5222
5869
  return true;
5223
5870
  }
5871
+ recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
5872
+ if (ap.inbox.length === 0) return false;
5873
+ if (ap.expectedTerminationReason === "stalled_recovery") {
5874
+ return true;
5875
+ }
5876
+ const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
5877
+ const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
5878
+ const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
5879
+ if (!canRestartStalledProcess) return false;
5880
+ const staleForMs = Date.now() - ap.lastRuntimeEventAt;
5881
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
5882
+ const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
5883
+ ap.runtimeProgressStaleSince ??= Date.now();
5884
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
5885
+ ageMs: staleForMs,
5886
+ staleForMinutes,
5887
+ lastActivity: ap.lastActivity,
5888
+ pendingMessages: ap.inbox.length,
5889
+ recovery: "terminate_for_queued_message"
5890
+ });
5891
+ this.endRuntimeTrace(ap, "error", {
5892
+ outcome: "runtime-stalled",
5893
+ ageMs: staleForMs,
5894
+ lastActivity: ap.lastActivity,
5895
+ pendingMessages: ap.inbox.length,
5896
+ recovery: "terminate_for_queued_message"
5897
+ });
5898
+ ap.expectedTerminationReason = "stalled_recovery";
5899
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
5900
+ logger.warn(
5901
+ `[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
5902
+ );
5903
+ this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
5904
+ try {
5905
+ ap.process.kill("SIGTERM");
5906
+ } catch (err) {
5907
+ const reason = err instanceof Error ? err.message : String(err);
5908
+ logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
5909
+ return false;
5910
+ }
5911
+ return true;
5912
+ }
5224
5913
  /** Handle a single ParsedEvent from any runtime driver */
5225
5914
  handleParsedEvent(agentId, event, driver) {
5226
5915
  const ap = this.agents.get(agentId);
@@ -5322,7 +6011,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5322
6011
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
5323
6012
  this.flushPendingTrajectory(agentId);
5324
6013
  if (ap) {
5325
- this.releaseAgentStartPermit(agentId, "initial turn ended");
5326
6014
  this.clearGatedInFlightBatch(agentId, ap, "turn_end");
5327
6015
  if (event.sessionId) ap.sessionId = event.sessionId;
5328
6016
  ap.gatedSteering.outstandingToolUses = 0;
@@ -5345,7 +6033,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5345
6033
  }
5346
6034
  } else {
5347
6035
  ap.isIdle = true;
5348
- this.broadcastActivity(agentId, "online", "Idle");
6036
+ if (ap.lastRuntimeError) {
6037
+ this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
6038
+ } else {
6039
+ this.broadcastActivity(agentId, "online", "Idle");
6040
+ }
5349
6041
  }
5350
6042
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
5351
6043
  if (ap.driver.terminateProcessOnTurnEnd) {
@@ -5384,6 +6076,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5384
6076
  }
5385
6077
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
5386
6078
  this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6079
+ if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6080
+ ap.isIdle = true;
6081
+ ap.pendingNotificationCount = 0;
6082
+ if (ap.notificationTimer) {
6083
+ clearTimeout(ap.notificationTimer);
6084
+ ap.notificationTimer = null;
6085
+ }
6086
+ logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
6087
+ }
5387
6088
  }
5388
6089
  this.broadcastActivity(agentId, "error", event.message, [
5389
6090
  { kind: "text", text: `Error: ${event.message}` }
@@ -5430,20 +6131,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5430
6131
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
5431
6132
  if (encoded) {
5432
6133
  ap.process.stdin?.write(encoded + "\n");
6134
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6135
+ agentId,
6136
+ runtime: ap.config.runtime,
6137
+ model: ap.config.model,
6138
+ launchId: ap.launchId || void 0,
6139
+ outcome: "written",
6140
+ mode: "busy",
6141
+ pending_notification_count: count,
6142
+ session_id_present: true
6143
+ });
6144
+ } else {
6145
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6146
+ agentId,
6147
+ runtime: ap.config.runtime,
6148
+ model: ap.config.model,
6149
+ launchId: ap.launchId || void 0,
6150
+ outcome: "encode_failed",
6151
+ mode: "busy",
6152
+ pending_notification_count: count,
6153
+ session_id_present: true
6154
+ }, "error");
5433
6155
  }
5434
6156
  }
5435
6157
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
5436
6158
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
5437
6159
  if (messages.length === 0) return true;
6160
+ const split = this.splitRuntimeProfileControlBatch(messages);
6161
+ if (split.deferredMessages.length > 0) {
6162
+ ap.inbox.unshift(...split.deferredMessages);
6163
+ ap.pendingNotificationCount += split.deferredMessages.length;
6164
+ messages = split.nextMessages;
6165
+ this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
6166
+ agentId,
6167
+ launchId: ap.launchId || void 0,
6168
+ runtime: ap.config.runtime,
6169
+ mode,
6170
+ delivered_control_messages_count: messages.length,
6171
+ deferred_messages_count: split.deferredMessages.length,
6172
+ inbox_count: ap.inbox.length,
6173
+ pending_notification_count: ap.pendingNotificationCount
6174
+ });
6175
+ }
6176
+ const traceAttrs = {
6177
+ agentId,
6178
+ launchId: ap.launchId || void 0,
6179
+ runtime: ap.config.runtime,
6180
+ model: ap.config.model,
6181
+ mode,
6182
+ messages_count: messages.length,
6183
+ session_id_present: Boolean(ap.sessionId),
6184
+ inbox_count: ap.inbox.length,
6185
+ pending_notification_count: ap.pendingNotificationCount,
6186
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
6187
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
6188
+ ...this.messagesTraceAttrs(messages)
6189
+ };
5438
6190
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
5439
6191
 
5440
6192
  ${formatIncomingMessage(messages[0], ap.driver)}
5441
6193
 
5442
- Respond as appropriate. Complete all your work before stopping.` : `New messages received:
6194
+ Respond as appropriate. Complete all your work before stopping.
6195
+ ${RESPONSE_TARGET_HINT}` : `New messages received:
5443
6196
 
5444
6197
  ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
5445
6198
 
5446
- Respond as appropriate. Complete all your work before stopping.`);
6199
+ Respond as appropriate. Complete all your work before stopping.
6200
+ ${RESPONSE_TARGET_HINT}`);
5447
6201
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
5448
6202
  if (!encoded) {
5449
6203
  ap.inbox.unshift(...messages);
@@ -5453,14 +6207,27 @@ Respond as appropriate. Complete all your work before stopping.`);
5453
6207
  logger.warn(
5454
6208
  `[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
5455
6209
  );
6210
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6211
+ ...traceAttrs,
6212
+ outcome: "encode_failed",
6213
+ requeued_messages_count: messages.length
6214
+ }, "error");
5456
6215
  return false;
5457
6216
  }
5458
6217
  const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
5459
6218
  logger.info(
5460
6219
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
5461
6220
  );
6221
+ if (this.containsOrdinaryInboxMessage(messages)) {
6222
+ ap.lastRuntimeError = null;
6223
+ }
5462
6224
  ap.process.stdin?.write(encoded + "\n");
5463
6225
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
6226
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6227
+ ...traceAttrs,
6228
+ outcome: "written",
6229
+ stdin_write_attempted: true
6230
+ });
5464
6231
  return true;
5465
6232
  }
5466
6233
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -5505,6 +6272,16 @@ var systemClock = {
5505
6272
  clearTimeout: (timer) => clearTimeout(timer)
5506
6273
  };
5507
6274
  var INBOUND_WATCHDOG_MS = 7e4;
6275
+ function durationMsBucket(ms) {
6276
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
6277
+ if (ms === 0) return "0";
6278
+ if (ms <= 1e3) return "1s";
6279
+ if (ms <= 1e4) return "1s-10s";
6280
+ if (ms <= 3e4) return "10s-30s";
6281
+ if (ms <= 6e4) return "30s-60s";
6282
+ if (ms <= 12e4) return "60s-120s";
6283
+ return "120s+";
6284
+ }
5508
6285
  var DaemonConnection = class {
5509
6286
  ws = null;
5510
6287
  options;
@@ -5516,6 +6293,8 @@ var DaemonConnection = class {
5516
6293
  shouldConnect = true;
5517
6294
  reconnectAttempt = 0;
5518
6295
  lastDroppedSendLogAt = 0;
6296
+ lastInboundAt = null;
6297
+ lastInboundMessageKind = null;
5519
6298
  constructor(options) {
5520
6299
  this.options = options;
5521
6300
  this.clock = options.clock ?? systemClock;
@@ -5550,6 +6329,10 @@ var DaemonConnection = class {
5550
6329
  this.lastDroppedSendLogAt = now;
5551
6330
  logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
5552
6331
  }
6332
+ this.trace("daemon.connection.outbound_dropped", {
6333
+ message_type: msg.type,
6334
+ ws_ready_state: this.ws?.readyState ?? null
6335
+ });
5553
6336
  }
5554
6337
  get connected() {
5555
6338
  return this.ws?.readyState === WebSocket.OPEN;
@@ -5563,25 +6346,49 @@ var DaemonConnection = class {
5563
6346
  if (wsOptions?.agent) {
5564
6347
  logger.info("[Daemon] Using configured proxy for WebSocket connection");
5565
6348
  }
6349
+ this.trace("daemon.connection.connecting", {
6350
+ reconnect_attempt: this.reconnectAttempt,
6351
+ server_url_present: Boolean(this.options.serverUrl),
6352
+ proxy_present: Boolean(wsOptions?.agent)
6353
+ });
5566
6354
  const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
5567
6355
  this.ws = ws;
5568
6356
  ws.on("open", () => {
5569
6357
  if (this.ws !== ws) return;
5570
6358
  if (!this.shouldConnect) return;
5571
6359
  logger.info("[Daemon] Connected to server");
6360
+ const priorReconnectAttempt = this.reconnectAttempt;
5572
6361
  this.reconnectAttempt = 0;
5573
6362
  this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
6363
+ this.markInbound("websocket_open");
5574
6364
  this.resetWatchdog();
6365
+ this.trace("daemon.connection.connected", {
6366
+ reconnect_attempt: priorReconnectAttempt,
6367
+ inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
6368
+ });
5575
6369
  this.options.onConnect();
5576
6370
  });
5577
6371
  ws.on("message", (data) => {
5578
6372
  if (this.ws !== ws) return;
5579
- this.resetWatchdog();
6373
+ let messageKind = "unknown";
5580
6374
  try {
5581
6375
  const msg = JSON.parse(data.toString());
6376
+ messageKind = msg.type;
6377
+ this.markInbound(messageKind);
6378
+ this.resetWatchdog();
6379
+ this.trace("daemon.connection.inbound_received", {
6380
+ message_type: messageKind,
6381
+ last_inbound_age_ms_bucket: "0"
6382
+ });
5582
6383
  this.options.onMessage(msg);
5583
6384
  } catch (err) {
6385
+ this.markInbound("invalid_json");
6386
+ this.resetWatchdog();
5584
6387
  logger.error("[Daemon] Invalid message from server", err);
6388
+ this.trace("daemon.connection.invalid_message", {
6389
+ error_class: err instanceof Error ? err.name : typeof err,
6390
+ last_inbound_message_kind: "invalid_json"
6391
+ }, "error");
5585
6392
  }
5586
6393
  });
5587
6394
  ws.on("close", (code, reasonBuffer) => {
@@ -5592,12 +6399,23 @@ var DaemonConnection = class {
5592
6399
  logger.warn(
5593
6400
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
5594
6401
  );
6402
+ this.trace("daemon.connection.disconnected", {
6403
+ close_code: code,
6404
+ close_reason_present: Boolean(reason),
6405
+ reconnecting: this.shouldConnect,
6406
+ reconnect_attempt: this.reconnectAttempt,
6407
+ last_inbound_message_kind: this.lastInboundMessageKind,
6408
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
6409
+ }, this.shouldConnect ? "cancelled" : "ok");
5595
6410
  this.options.onDisconnect();
5596
6411
  this.scheduleReconnect();
5597
6412
  });
5598
6413
  ws.on("error", (err) => {
5599
6414
  if (this.ws !== ws) return;
5600
6415
  logger.error(`[Daemon] WebSocket error: ${err.message}`);
6416
+ this.trace("daemon.connection.error", {
6417
+ error_class: err.name || "Error"
6418
+ }, "error");
5601
6419
  });
5602
6420
  }
5603
6421
  scheduleReconnect() {
@@ -5605,6 +6423,10 @@ var DaemonConnection = class {
5605
6423
  if (this.reconnectTimer) return;
5606
6424
  this.reconnectAttempt += 1;
5607
6425
  logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
6426
+ this.trace("daemon.connection.reconnect_scheduled", {
6427
+ reconnect_attempt: this.reconnectAttempt,
6428
+ delay_ms: this.reconnectDelay
6429
+ });
5608
6430
  this.reconnectTimer = this.clock.setTimeout(() => {
5609
6431
  this.reconnectTimer = null;
5610
6432
  this.doConnect();
@@ -5616,6 +6438,13 @@ var DaemonConnection = class {
5616
6438
  const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
5617
6439
  this.watchdogTimer = this.clock.setTimeout(() => {
5618
6440
  logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
6441
+ this.trace("daemon.connection.watchdog_timeout", {
6442
+ inbound_watchdog_ms: ms,
6443
+ last_inbound_message_kind: this.lastInboundMessageKind,
6444
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
6445
+ ws_ready_state: this.ws?.readyState ?? null,
6446
+ reconnecting: this.shouldConnect
6447
+ }, "error");
5619
6448
  try {
5620
6449
  this.ws?.terminate();
5621
6450
  } catch {
@@ -5628,6 +6457,16 @@ var DaemonConnection = class {
5628
6457
  this.watchdogTimer = null;
5629
6458
  }
5630
6459
  }
6460
+ markInbound(messageKind) {
6461
+ this.lastInboundAt = this.clock.now();
6462
+ this.lastInboundMessageKind = messageKind;
6463
+ }
6464
+ lastInboundAgeBucket() {
6465
+ return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
6466
+ }
6467
+ trace(name, attrs, status = "ok") {
6468
+ this.options.onTraceEvent?.(name, attrs, status);
6469
+ }
5631
6470
  };
5632
6471
 
5633
6472
  // src/reminderCache.ts
@@ -5711,10 +6550,10 @@ var ReminderCache = class {
5711
6550
 
5712
6551
  // src/machineLock.ts
5713
6552
  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";
6553
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
6554
+ import os6 from "os";
5716
6555
  import path12 from "path";
5717
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
6556
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
5718
6557
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
5719
6558
  var DaemonMachineLockConflictError = class extends Error {
5720
6559
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -5737,14 +6576,14 @@ function ownerPath(lockDir) {
5737
6576
  }
5738
6577
  function readOwner(lockDir) {
5739
6578
  try {
5740
- return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
6579
+ return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
5741
6580
  } catch {
5742
6581
  return null;
5743
6582
  }
5744
6583
  }
5745
6584
  function lockAgeMs(lockDir) {
5746
6585
  try {
5747
- return Date.now() - statSync2(lockDir).mtimeMs;
6586
+ return Date.now() - statSync3(lockDir).mtimeMs;
5748
6587
  } catch {
5749
6588
  return null;
5750
6589
  }
@@ -5773,7 +6612,7 @@ function acquireDaemonMachineLock(options) {
5773
6612
  const owner = {
5774
6613
  pid: process.pid,
5775
6614
  token,
5776
- hostname: os5.hostname(),
6615
+ hostname: os6.hostname(),
5777
6616
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5778
6617
  serverUrl: options.serverUrl,
5779
6618
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5815,6 +6654,418 @@ function acquireDaemonMachineLock(options) {
5815
6654
  throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
5816
6655
  }
5817
6656
 
6657
+ // src/localTraceSink.ts
6658
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6659
+ import path13 from "path";
6660
+ var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6661
+ var DEFAULT_MAX_FILES = 8;
6662
+ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6663
+ "serverId",
6664
+ "machineId",
6665
+ "agentId",
6666
+ "messageId",
6667
+ "launchId",
6668
+ "uploadId",
6669
+ "bundleId",
6670
+ "deliveryId",
6671
+ "deliveryCorrelationId",
6672
+ "delivery_correlation_id"
6673
+ ]);
6674
+ var LocalRotatingTraceSink = class {
6675
+ traceDir;
6676
+ maxFileBytes;
6677
+ maxFiles;
6678
+ currentFile = null;
6679
+ currentSize = 0;
6680
+ sequence = 0;
6681
+ constructor(options) {
6682
+ this.traceDir = path13.join(options.machineDir, "traces");
6683
+ this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6684
+ this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
6685
+ }
6686
+ record(span) {
6687
+ try {
6688
+ const line = `${JSON.stringify(toLocalTraceRecord(span))}
6689
+ `;
6690
+ this.ensureFile(Buffer.byteLength(line));
6691
+ appendFileSync(this.currentFile, line, { encoding: "utf8" });
6692
+ this.currentSize += Buffer.byteLength(line);
6693
+ } catch {
6694
+ }
6695
+ }
6696
+ getCurrentFile() {
6697
+ return this.currentFile;
6698
+ }
6699
+ ensureFile(nextBytes) {
6700
+ mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
6701
+ if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
6702
+ this.currentFile = path13.join(
6703
+ this.traceDir,
6704
+ `daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6705
+ );
6706
+ writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
6707
+ this.currentSize = statSync4(this.currentFile).size;
6708
+ this.pruneOldFiles();
6709
+ }
6710
+ }
6711
+ pruneOldFiles() {
6712
+ const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
6713
+ const excess = files.length - this.maxFiles;
6714
+ if (excess <= 0) return;
6715
+ for (const file of files.slice(0, excess)) {
6716
+ rmSync3(path13.join(this.traceDir, file), { force: true });
6717
+ }
6718
+ }
6719
+ };
6720
+ function safeTimestamp(timeMs) {
6721
+ return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
6722
+ }
6723
+ function toLocalTraceRecord(span) {
6724
+ return {
6725
+ type: "span",
6726
+ schema_version: 1,
6727
+ trace_id: span.context.traceId,
6728
+ span_id: span.context.spanId,
6729
+ parent_span_id: span.context.parentSpanId,
6730
+ name: span.name,
6731
+ surface: span.surface,
6732
+ kind: span.kind,
6733
+ status: span.status,
6734
+ start_time: new Date(span.startTimeMs).toISOString(),
6735
+ end_time: new Date(span.endTimeMs).toISOString(),
6736
+ duration_ms: span.durationMs,
6737
+ attrs: sanitizeAttrs(span.attrs),
6738
+ events: span.events.map(sanitizeEvent)
6739
+ };
6740
+ }
6741
+ function sanitizeEvent(event) {
6742
+ return {
6743
+ name: event.name,
6744
+ time: new Date(event.timeMs).toISOString(),
6745
+ attrs: sanitizeAttrs(event.attrs)
6746
+ };
6747
+ }
6748
+ function sanitizeAttrs(attrs) {
6749
+ if (!attrs) return void 0;
6750
+ const sanitized = {};
6751
+ for (const [key, value] of Object.entries(attrs)) {
6752
+ if (isDiagnosticIdAttr(key)) {
6753
+ if (value === null || value === void 0 || value === "") continue;
6754
+ sanitized[key] = sanitizeValue(value);
6755
+ continue;
6756
+ }
6757
+ if (shouldDropAttr(key)) continue;
6758
+ sanitized[key] = sanitizeValue(value);
6759
+ }
6760
+ return sanitized;
6761
+ }
6762
+ function sanitizeValue(value) {
6763
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6764
+ return value;
6765
+ }
6766
+ if (Array.isArray(value)) {
6767
+ return { items_count: value.length };
6768
+ }
6769
+ if (typeof value === "object") {
6770
+ return { object_present: true };
6771
+ }
6772
+ return String(value);
6773
+ }
6774
+ function shouldDropAttr(key) {
6775
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
6776
+ if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
6777
+ return true;
6778
+ }
6779
+ if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
6780
+ return false;
6781
+ }
6782
+ if (/(^|_)id$/.test(normalized)) {
6783
+ return true;
6784
+ }
6785
+ 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);
6786
+ }
6787
+ function isDiagnosticIdAttr(key) {
6788
+ return DIAGNOSTIC_ID_ATTRS.has(key);
6789
+ }
6790
+
6791
+ // src/traceBundleUpload.ts
6792
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
6793
+ import { gzipSync } from "zlib";
6794
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
6795
+ import path14 from "path";
6796
+
6797
+ // src/directUploadCapability.ts
6798
+ function joinUrl(base, path16) {
6799
+ return `${base.replace(/\/+$/, "")}${path16}`;
6800
+ }
6801
+ function jsonHeaders(apiKey) {
6802
+ return {
6803
+ "Content-Type": "application/json",
6804
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
6805
+ };
6806
+ }
6807
+ async function requestDaemonScopeAttestation({
6808
+ serverUrl,
6809
+ apiKey,
6810
+ scope,
6811
+ metadata,
6812
+ fetchImpl = fetch,
6813
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6814
+ }) {
6815
+ const { response, data } = await executeJsonRequest(
6816
+ joinUrl(serverUrl, "/internal/machine/scope-attestation"),
6817
+ {
6818
+ method: "POST",
6819
+ headers: jsonHeaders(apiKey),
6820
+ body: JSON.stringify({
6821
+ scope,
6822
+ ...metadata ? { metadata } : {}
6823
+ })
6824
+ },
6825
+ {
6826
+ toolName: "daemon_direct_upload.scope_attestation",
6827
+ target: scope,
6828
+ timeoutMs,
6829
+ fetchImpl
6830
+ }
6831
+ );
6832
+ if (!response.ok) {
6833
+ throw new Error(`Failed to request daemon scope attestation (${response.status})`);
6834
+ }
6835
+ return data;
6836
+ }
6837
+ async function createDirectUploadSession({
6838
+ serverUrl,
6839
+ apiKey,
6840
+ workerUrl,
6841
+ scope,
6842
+ createPath = "/api/uploads",
6843
+ body,
6844
+ attestationMetadata,
6845
+ fetchImpl = fetch,
6846
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6847
+ }) {
6848
+ const capability = await requestDaemonScopeAttestation({
6849
+ serverUrl,
6850
+ apiKey,
6851
+ scope,
6852
+ metadata: attestationMetadata,
6853
+ fetchImpl,
6854
+ timeoutMs
6855
+ });
6856
+ const { response, data } = await executeJsonRequest(
6857
+ joinUrl(workerUrl, createPath),
6858
+ {
6859
+ method: "POST",
6860
+ headers: jsonHeaders(),
6861
+ body: JSON.stringify({
6862
+ ...body,
6863
+ attestation: capability.attestation
6864
+ })
6865
+ },
6866
+ {
6867
+ toolName: "daemon_direct_upload.create",
6868
+ target: capability.audience,
6869
+ timeoutMs,
6870
+ fetchImpl
6871
+ }
6872
+ );
6873
+ if (!response.ok) {
6874
+ throw new Error(`Failed to create direct upload session (${response.status})`);
6875
+ }
6876
+ return { capability, response: data };
6877
+ }
6878
+ async function uploadWithSignedCapability({
6879
+ serverUrl,
6880
+ apiKey,
6881
+ workerUrl,
6882
+ scope,
6883
+ createPath = "/api/uploads",
6884
+ createBody,
6885
+ attestationMetadata,
6886
+ uploadBody,
6887
+ fetchImpl = fetch,
6888
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6889
+ }) {
6890
+ const { capability, response: session } = await createDirectUploadSession({
6891
+ serverUrl,
6892
+ apiKey,
6893
+ workerUrl,
6894
+ scope,
6895
+ createPath,
6896
+ body: createBody,
6897
+ attestationMetadata,
6898
+ fetchImpl,
6899
+ timeoutMs
6900
+ });
6901
+ const { response: uploadResponse } = await executeResponseRequest(
6902
+ session.upload.url,
6903
+ {
6904
+ method: session.upload.method,
6905
+ headers: session.upload.headers ?? {},
6906
+ body: uploadBody
6907
+ },
6908
+ {
6909
+ toolName: "daemon_direct_upload.put",
6910
+ target: capability.audience,
6911
+ timeoutMs,
6912
+ fetchImpl
6913
+ }
6914
+ );
6915
+ if (!uploadResponse.ok) {
6916
+ throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
6917
+ }
6918
+ return { capability, session, uploadResponse };
6919
+ }
6920
+
6921
+ // src/traceBundleUpload.ts
6922
+ var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
6923
+ var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
6924
+ var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
6925
+ var DEFAULT_MAX_FILES_PER_RUN = 4;
6926
+ var DaemonTraceBundleUploader = class {
6927
+ options;
6928
+ timer = null;
6929
+ constructor(options) {
6930
+ this.options = options;
6931
+ }
6932
+ start() {
6933
+ if (this.timer) return;
6934
+ void this.uploadOnce();
6935
+ this.timer = setInterval(() => {
6936
+ void this.uploadOnce();
6937
+ }, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
6938
+ }
6939
+ stop() {
6940
+ if (!this.timer) return;
6941
+ clearInterval(this.timer);
6942
+ this.timer = null;
6943
+ }
6944
+ async uploadOnce() {
6945
+ const files = await this.findUploadCandidates();
6946
+ let uploaded = 0;
6947
+ for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
6948
+ if (await this.uploadFile(file)) uploaded += 1;
6949
+ }
6950
+ return { attempted: files.length, uploaded };
6951
+ }
6952
+ async findUploadCandidates() {
6953
+ const traceDir = path14.join(this.options.machineDir, "traces");
6954
+ let names;
6955
+ try {
6956
+ names = await readdir3(traceDir);
6957
+ } catch {
6958
+ return [];
6959
+ }
6960
+ const now = Date.now();
6961
+ const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
6962
+ const currentFile = this.options.currentFileProvider?.();
6963
+ const candidates = [];
6964
+ for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
6965
+ const file = path14.join(traceDir, name);
6966
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
6967
+ if (await this.isUploaded(file)) continue;
6968
+ try {
6969
+ const info = await stat3(file);
6970
+ if (!info.isFile() || info.size <= 0) continue;
6971
+ if (now - info.mtimeMs < minAgeMs) continue;
6972
+ candidates.push(file);
6973
+ } catch {
6974
+ }
6975
+ }
6976
+ return candidates;
6977
+ }
6978
+ async uploadFile(file) {
6979
+ const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
6980
+ surface: "daemon",
6981
+ kind: "producer",
6982
+ attrs: {
6983
+ file_present: true,
6984
+ worker_url_present: Boolean(this.options.workerUrl)
6985
+ }
6986
+ });
6987
+ try {
6988
+ const raw = await readFile2(file);
6989
+ if (raw.byteLength === 0) {
6990
+ span?.end("cancelled", { attrs: { outcome: "empty" } });
6991
+ return false;
6992
+ }
6993
+ const gzipped = gzipSync(raw);
6994
+ const bundleSha256 = sha256Hex(gzipped);
6995
+ const bundleId = randomUUID3();
6996
+ await uploadWithSignedCapability({
6997
+ serverUrl: this.options.serverUrl,
6998
+ apiKey: this.options.apiKey,
6999
+ workerUrl: this.options.workerUrl,
7000
+ scope: TRACE_UPLOAD_SCOPE,
7001
+ createPath: "/api/trace-bundles",
7002
+ attestationMetadata: {
7003
+ bundleId,
7004
+ bundleSha256,
7005
+ bundleSizeBytes: gzipped.byteLength
7006
+ },
7007
+ createBody: {
7008
+ bundleSha256,
7009
+ bundleSizeBytes: gzipped.byteLength
7010
+ },
7011
+ uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
7012
+ fetchImpl: this.options.fetchImpl
7013
+ });
7014
+ await this.markUploaded(file, {
7015
+ bundleId,
7016
+ bundleSha256,
7017
+ bundleSizeBytes: gzipped.byteLength
7018
+ });
7019
+ span?.end("ok", {
7020
+ attrs: {
7021
+ bundleId,
7022
+ bundle_size_bytes: gzipped.byteLength
7023
+ }
7024
+ });
7025
+ return true;
7026
+ } catch (err) {
7027
+ span?.end("error", {
7028
+ attrs: {
7029
+ error_class: err instanceof Error ? err.name : "Error",
7030
+ error_message_present: err instanceof Error && Boolean(err.message)
7031
+ }
7032
+ });
7033
+ return false;
7034
+ }
7035
+ }
7036
+ uploadStatePath(file) {
7037
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7038
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
7039
+ }
7040
+ async isUploaded(file) {
7041
+ try {
7042
+ await stat3(this.uploadStatePath(file));
7043
+ return true;
7044
+ } catch {
7045
+ return false;
7046
+ }
7047
+ }
7048
+ async markUploaded(file, metadata) {
7049
+ const stateFile = this.uploadStatePath(file);
7050
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
7051
+ await writeFile2(stateFile, `${JSON.stringify({
7052
+ file: path14.basename(file),
7053
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7054
+ ...metadata
7055
+ }, null, 2)}
7056
+ `, { mode: 384 });
7057
+ }
7058
+ };
7059
+ function sha256Hex(body) {
7060
+ return createHash2("sha256").update(body).digest("hex");
7061
+ }
7062
+ function readPositiveIntegerEnv2(name, fallback) {
7063
+ const value = process.env[name];
7064
+ if (!value) return fallback;
7065
+ const parsed = Number(value);
7066
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
7067
+ }
7068
+
5818
7069
  // src/core.ts
5819
7070
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
5820
7071
  function parseDaemonCliArgs(args) {
@@ -5836,59 +7087,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5836
7087
  }
5837
7088
  }
5838
7089
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5839
- const dirname = path13.dirname(fileURLToPath(moduleUrl));
5840
- const jsPath = path13.resolve(dirname, "chat-bridge.js");
7090
+ const dirname = path15.dirname(fileURLToPath(moduleUrl));
7091
+ const jsPath = path15.resolve(dirname, "chat-bridge.js");
5841
7092
  try {
5842
7093
  accessSync(jsPath);
5843
7094
  return jsPath;
5844
7095
  } catch {
5845
- return path13.resolve(dirname, "chat-bridge.ts");
7096
+ return path15.resolve(dirname, "chat-bridge.ts");
5846
7097
  }
5847
7098
  }
5848
7099
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5849
- const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5850
- const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
7100
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7101
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
5851
7102
  try {
5852
7103
  accessSync(bundledDistPath);
5853
7104
  return bundledDistPath;
5854
7105
  } catch {
5855
- const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7106
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5856
7107
  accessSync(workspaceDistPath);
5857
7108
  return workspaceDistPath;
5858
7109
  }
5859
7110
  }
5860
- function detectRuntimes() {
7111
+ function detectRuntimes(tracer = noopTracer) {
5861
7112
  const ids = [];
5862
7113
  const versions = {};
7114
+ const span = tracer.startSpan("daemon.runtime.detect", {
7115
+ surface: "daemon",
7116
+ kind: "internal",
7117
+ attrs: {
7118
+ known_runtime_count: RUNTIMES.length
7119
+ }
7120
+ });
5863
7121
  for (const runtime of RUNTIMES) {
5864
7122
  const driver = getDriver(runtime.id);
7123
+ let probeErrorPresent = false;
5865
7124
  try {
5866
7125
  if (driver.probe) {
5867
7126
  const probe = driver.probe();
5868
7127
  if (!probe.available) {
5869
7128
  if (probe.version) versions[runtime.id] = probe.version;
7129
+ span.addEvent("daemon.runtime.detect.checked", {
7130
+ runtime: runtime.id,
7131
+ outcome: "unavailable",
7132
+ version_present: Boolean(probe.version),
7133
+ binary_path_present: false
7134
+ });
5870
7135
  continue;
5871
7136
  }
5872
7137
  ids.push(runtime.id);
5873
7138
  if (probe.version) versions[runtime.id] = probe.version;
7139
+ span.addEvent("daemon.runtime.detect.checked", {
7140
+ runtime: runtime.id,
7141
+ outcome: "available",
7142
+ version_present: Boolean(probe.version),
7143
+ binary_path_present: false
7144
+ });
5874
7145
  continue;
5875
7146
  }
5876
7147
  } catch {
7148
+ probeErrorPresent = true;
5877
7149
  }
5878
7150
  const detectionBinaries = [runtime.binary];
7151
+ let detectedByPath = false;
5879
7152
  for (const binary of detectionBinaries) {
5880
7153
  const resolved = resolveCommandOnPath(binary);
5881
7154
  if (!resolved) continue;
5882
7155
  ids.push(runtime.id);
7156
+ detectedByPath = true;
5883
7157
  const version = readCommandVersion(binary);
5884
7158
  if (version) {
5885
7159
  versions[runtime.id] = version;
5886
7160
  }
7161
+ span.addEvent("daemon.runtime.detect.checked", {
7162
+ runtime: runtime.id,
7163
+ outcome: "available",
7164
+ version_present: Boolean(version),
7165
+ binary_path_present: true,
7166
+ probe_error_present: probeErrorPresent
7167
+ });
5887
7168
  break;
5888
7169
  }
7170
+ if (!detectedByPath) {
7171
+ span.addEvent("daemon.runtime.detect.checked", {
7172
+ runtime: runtime.id,
7173
+ outcome: "unavailable",
7174
+ version_present: false,
7175
+ binary_path_present: false,
7176
+ probe_error_present: probeErrorPresent
7177
+ });
7178
+ }
5889
7179
  }
7180
+ span.end("ok", {
7181
+ attrs: {
7182
+ detected_runtime_count: ids.length
7183
+ }
7184
+ });
5890
7185
  return { ids, versions };
5891
7186
  }
7187
+ function readPositiveIntegerEnv3(name, fallback) {
7188
+ const raw = process.env[name];
7189
+ if (!raw) return fallback;
7190
+ const parsed = Number(raw);
7191
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
7192
+ return Math.floor(parsed);
7193
+ }
5892
7194
  function formatChannelTarget(msg) {
5893
7195
  return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
5894
7196
  }
@@ -5938,14 +7240,18 @@ var DaemonCore = class {
5938
7240
  connection;
5939
7241
  reminderCache;
5940
7242
  tracer;
7243
+ injectedTracer;
5941
7244
  machineLock = null;
7245
+ localTraceSink = null;
7246
+ traceBundleUploader = null;
5942
7247
  constructor(options) {
5943
7248
  this.options = options;
5944
7249
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
5945
7250
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
5946
7251
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
5947
- this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
7252
+ this.injectedTracer = Boolean(options.tracer);
5948
7253
  this.tracer = options.tracer ?? noopTracer;
7254
+ this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
5949
7255
  this.reminderCache = new ReminderCache({
5950
7256
  clock: options.reminderClock,
5951
7257
  onFire: (job) => this.onReminderFire(job)
@@ -5955,7 +7261,8 @@ var DaemonCore = class {
5955
7261
  dataDir: options.dataDir,
5956
7262
  serverUrl: options.serverUrl,
5957
7263
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
5958
- slockCliPath: this.slockCliPath
7264
+ slockCliPath: this.slockCliPath,
7265
+ tracer: this.tracer
5959
7266
  };
5960
7267
  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
7268
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -5965,15 +7272,48 @@ var DaemonCore = class {
5965
7272
  ...options.connectionOptions,
5966
7273
  onMessage: (msg) => this.handleMessage(msg),
5967
7274
  onConnect: () => this.handleConnect(),
5968
- onDisconnect: () => this.handleDisconnect()
7275
+ onDisconnect: () => this.handleDisconnect(),
7276
+ onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
5969
7277
  });
5970
7278
  this.connection = connection;
5971
7279
  }
5972
7280
  resolveMachineStateRoot() {
5973
7281
  if (this.options.machineStateDir) return this.options.machineStateDir;
5974
- if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
7282
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
5975
7283
  return DEFAULT_MACHINE_STATE_ROOT;
5976
7284
  }
7285
+ shouldEnableLocalTrace() {
7286
+ if (this.injectedTracer) return false;
7287
+ if (!this.options.localTrace) return false;
7288
+ return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
7289
+ }
7290
+ installLocalTraceSink(machineDir) {
7291
+ if (!this.shouldEnableLocalTrace()) return;
7292
+ this.localTraceSink = new LocalRotatingTraceSink({
7293
+ machineDir,
7294
+ maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
7295
+ maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
7296
+ });
7297
+ this.tracer = new BasicTracer({
7298
+ sink: this.localTraceSink
7299
+ });
7300
+ this.agentManager.setTracer(this.tracer);
7301
+ }
7302
+ installTraceBundleUploader(machineDir) {
7303
+ if (!this.shouldEnableLocalTrace()) return;
7304
+ if (this.traceBundleUploader) return;
7305
+ const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
7306
+ if (!workerUrl) return;
7307
+ this.traceBundleUploader = new DaemonTraceBundleUploader({
7308
+ machineDir,
7309
+ serverUrl: this.options.serverUrl,
7310
+ apiKey: this.options.apiKey,
7311
+ workerUrl,
7312
+ tracer: this.tracer,
7313
+ currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
7314
+ });
7315
+ this.traceBundleUploader.start();
7316
+ }
5977
7317
  start() {
5978
7318
  logger.info("[Slock Daemon] Starting...");
5979
7319
  if (!this.machineLock) {
@@ -5983,10 +7323,24 @@ var DaemonCore = class {
5983
7323
  rootDir: this.resolveMachineStateRoot()
5984
7324
  });
5985
7325
  logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
7326
+ this.installLocalTraceSink(this.machineLock.machineDir);
7327
+ this.installTraceBundleUploader(this.machineLock.machineDir);
7328
+ const span = this.tracer.startSpan("daemon.lifecycle.start", {
7329
+ surface: "daemon",
7330
+ kind: "internal",
7331
+ attrs: {
7332
+ machine_dir_present: true,
7333
+ local_trace_enabled: this.shouldEnableLocalTrace()
7334
+ }
7335
+ });
7336
+ span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
7337
+ span.end("ok");
5986
7338
  }
5987
7339
  try {
5988
7340
  this.connection.connect();
5989
7341
  } catch (err) {
7342
+ this.traceBundleUploader?.stop();
7343
+ this.traceBundleUploader = null;
5990
7344
  this.machineLock.release();
5991
7345
  this.machineLock = null;
5992
7346
  throw err;
@@ -5994,13 +7348,24 @@ var DaemonCore = class {
5994
7348
  }
5995
7349
  async stop() {
5996
7350
  logger.info("[Slock Daemon] Shutting down...");
7351
+ const span = this.tracer.startSpan("daemon.lifecycle.stop", {
7352
+ surface: "daemon",
7353
+ kind: "internal",
7354
+ attrs: { machine_lock_present: Boolean(this.machineLock) }
7355
+ });
5997
7356
  this.reminderCache.clear();
7357
+ this.traceBundleUploader?.stop();
7358
+ this.traceBundleUploader = null;
5998
7359
  try {
5999
7360
  await this.agentManager.stopAll();
7361
+ span.addEvent("daemon.agents.stopped");
6000
7362
  } finally {
6001
7363
  this.connection.disconnect();
7364
+ span.addEvent("daemon.connection.disconnect_requested");
6002
7365
  this.machineLock?.release();
7366
+ if (this.machineLock) span.addEvent("daemon.machine_lock.released");
6003
7367
  this.machineLock = null;
7368
+ span.end("ok");
6004
7369
  }
6005
7370
  }
6006
7371
  get connected() {
@@ -6009,6 +7374,14 @@ var DaemonCore = class {
6009
7374
  getRunningAgentIds() {
6010
7375
  return this.agentManager.getRunningAgentIds();
6011
7376
  }
7377
+ recordDaemonTrace(name, attrs, status = "ok") {
7378
+ const span = this.tracer.startSpan(name, {
7379
+ surface: "daemon",
7380
+ kind: "internal",
7381
+ attrs
7382
+ });
7383
+ span.end(status);
7384
+ }
6012
7385
  handleMessage(msg) {
6013
7386
  const summary = summarizeIncomingMessage(msg);
6014
7387
  logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
@@ -6038,50 +7411,88 @@ var DaemonCore = class {
6038
7411
  kind: "consumer",
6039
7412
  attrs: {
6040
7413
  agentId: msg.agentId,
7414
+ deliveryId: msg.deliveryId,
7415
+ delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
7416
+ messageId: msg.message.message_id,
7417
+ message_id_present: Boolean(msg.message.message_id),
6041
7418
  seq: msg.seq
6042
7419
  }
6043
7420
  });
6044
7421
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
6045
7422
  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");
7423
+ span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
7424
+ const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
7425
+ span.addEvent("daemon.deliver_to_agent_manager", { accepted });
7426
+ if (!accepted) {
7427
+ span.end("ok", { attrs: { outcome: "not-accepted" } });
7428
+ break;
7429
+ }
6049
7430
  const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
6050
7431
  span.addEvent("daemon.ack.sent", { seq: ackSeq });
6051
7432
  this.connection.send({
6052
7433
  type: "agent:deliver:ack",
6053
7434
  agentId: msg.agentId,
6054
7435
  seq: ackSeq,
6055
- traceparent: formatTraceparent(span.context)
7436
+ traceparent: formatTraceparent(span.context),
7437
+ deliveryId: msg.deliveryId
6056
7438
  });
6057
- span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
7439
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
6058
7440
  } catch (err) {
6059
- span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
7441
+ span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
6060
7442
  throw err;
6061
7443
  }
6062
7444
  break;
6063
7445
  }
6064
- case "agent:runtime_profile:migration":
7446
+ case "agent:runtime_profile:migration": {
7447
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7448
+ parent: parseTraceparent(msg.traceparent),
7449
+ surface: "daemon",
7450
+ kind: "consumer",
7451
+ attrs: {
7452
+ agentId: msg.agentId,
7453
+ control_kind: "migration",
7454
+ key_present: Boolean(msg.migrationKey),
7455
+ launchId: msg.launchId || void 0
7456
+ }
7457
+ });
6065
7458
  logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
6066
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
7459
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
7460
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
6067
7461
  break;
6068
- case "agent:runtime_profile:daemon_release_notice":
7462
+ }
7463
+ case "agent:runtime_profile:daemon_release_notice": {
7464
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7465
+ parent: parseTraceparent(msg.traceparent),
7466
+ surface: "daemon",
7467
+ kind: "consumer",
7468
+ attrs: {
7469
+ agentId: msg.agentId,
7470
+ control_kind: "daemon_release_notice",
7471
+ key_present: Boolean(msg.noticeKey),
7472
+ launchId: msg.launchId || void 0
7473
+ }
7474
+ });
6069
7475
  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);
7476
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
7477
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
6071
7478
  break;
7479
+ }
6072
7480
  case "agent:workspace:list":
6073
7481
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
6074
7482
  this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
6075
7483
  });
6076
7484
  break;
6077
7485
  case "agent:workspace:read":
6078
- this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
7486
+ this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
6079
7487
  this.connection.send({
6080
7488
  type: "agent:workspace:file_content",
6081
7489
  agentId: msg.agentId,
6082
7490
  requestId: msg.requestId,
6083
7491
  content,
6084
- binary
7492
+ binary,
7493
+ size,
7494
+ mimeType,
7495
+ encoding
6085
7496
  });
6086
7497
  }).catch(() => {
6087
7498
  this.connection.send({
@@ -6089,7 +7500,8 @@ var DaemonCore = class {
6089
7500
  agentId: msg.agentId,
6090
7501
  requestId: msg.requestId,
6091
7502
  content: null,
6092
- binary: false
7503
+ binary: false,
7504
+ size: 0
6093
7505
  });
6094
7506
  });
6095
7507
  break;
@@ -6165,35 +7577,59 @@ var DaemonCore = class {
6165
7577
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
6166
7578
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
6167
7579
  logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
7580
+ const runningAgentIds = this.agentManager.getRunningAgentIds();
7581
+ const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
7582
+ const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
6168
7583
  this.connection.send({
6169
7584
  type: "ready",
6170
7585
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
6171
7586
  runtimes,
6172
- runningAgents: this.agentManager.getRunningAgentIds(),
6173
- hostname: this.options.hostname ?? os6.hostname(),
6174
- os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
7587
+ runningAgents: runningAgentIds,
7588
+ hostname: this.options.hostname ?? os7.hostname(),
7589
+ os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
6175
7590
  daemonVersion: this.daemonVersion
6176
7591
  });
6177
- for (const agentId of this.agentManager.getRunningAgentIds()) {
7592
+ this.recordDaemonTrace("daemon.ready.sent", {
7593
+ runtimes_count: runtimes.length,
7594
+ running_agents_count: runningAgentIds.length,
7595
+ idle_agents_count: idleAgentSessions.length,
7596
+ runtime_profile_reports_count: runtimeProfileReports.length,
7597
+ daemon_version_present: Boolean(this.daemonVersion)
7598
+ });
7599
+ for (const agentId of runningAgentIds) {
6178
7600
  const sessionId = this.agentManager.getAgentSessionId(agentId);
6179
7601
  const launchId = this.agentManager.getAgentLaunchId(agentId);
6180
7602
  if (sessionId) {
6181
7603
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
6182
7604
  }
6183
7605
  }
6184
- for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
7606
+ for (const { agentId, sessionId, launchId } of idleAgentSessions) {
6185
7607
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
6186
7608
  }
6187
- for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
7609
+ for (const report of runtimeProfileReports) {
7610
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
7611
+ surface: "daemon",
7612
+ kind: "producer",
7613
+ attrs: {
7614
+ agentId: report.agentId,
7615
+ launchId: report.launchId || void 0,
7616
+ runtime: report.facts.runtime,
7617
+ model_present: Boolean(report.facts.model),
7618
+ session_ref_present: Boolean(report.facts.sessionRef),
7619
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
7620
+ }
7621
+ });
6188
7622
  this.connection.send({
6189
7623
  type: "agent:runtime_profile",
6190
7624
  agentId: report.agentId,
6191
7625
  facts: report.facts,
6192
- launchId: report.launchId || void 0
7626
+ launchId: report.launchId || void 0,
7627
+ traceparent: formatTraceparent(span.context)
6193
7628
  });
7629
+ span.end("ok");
6194
7630
  }
6195
- const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
6196
- for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
7631
+ const agentsForSnapshot = new Set(runningAgentIds);
7632
+ for (const { agentId } of idleAgentSessions) {
6197
7633
  agentsForSnapshot.add(agentId);
6198
7634
  }
6199
7635
  for (const agentId of agentsForSnapshot) {
@@ -6203,6 +7639,10 @@ var DaemonCore = class {
6203
7639
  }
6204
7640
  handleDisconnect() {
6205
7641
  logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
7642
+ this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
7643
+ running_agents_count: this.agentManager.getRunningAgentIds().length,
7644
+ idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
7645
+ }, "cancelled");
6206
7646
  this.options.lifecycleHooks?.onDisconnect?.();
6207
7647
  }
6208
7648
  };