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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,14 @@
1
1
  import {
2
+ DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
2
3
  buildWebSocketOptions,
4
+ executeJsonRequest,
5
+ executeResponseRequest,
3
6
  logger
4
- } from "./chunk-JG7ONJZ6.js";
7
+ } from "./chunk-Z3PCMYZO.js";
5
8
 
6
9
  // src/core.ts
7
- import path13 from "path";
8
- import os6 from "os";
10
+ import path15 from "path";
11
+ import os7 from "os";
9
12
  import { createRequire } from "module";
10
13
  import { accessSync } from "fs";
11
14
  import { fileURLToPath } from "url";
@@ -94,6 +97,103 @@ var NoopActiveSpan = class {
94
97
  }
95
98
  };
96
99
  var noopTracer = new NoopTracer();
100
+ var BasicTracer = class {
101
+ sink;
102
+ clock;
103
+ traceIdGenerator;
104
+ spanIdGenerator;
105
+ constructor({
106
+ sink,
107
+ clock = () => Date.now(),
108
+ traceIdGenerator = generateTraceId,
109
+ spanIdGenerator = generateSpanId
110
+ }) {
111
+ this.sink = sink;
112
+ this.clock = clock;
113
+ this.traceIdGenerator = traceIdGenerator;
114
+ this.spanIdGenerator = spanIdGenerator;
115
+ }
116
+ startSpan(name, options) {
117
+ const startTimeMs = options.startTimeMs ?? this.clock();
118
+ const context = createTraceContext({
119
+ parent: options.parent ?? null,
120
+ traceIdGenerator: this.traceIdGenerator,
121
+ spanIdGenerator: this.spanIdGenerator
122
+ });
123
+ return new RecordingActiveSpan({
124
+ context,
125
+ name,
126
+ surface: options.surface,
127
+ kind: options.kind ?? "internal",
128
+ attrs: options.attrs,
129
+ startTimeMs,
130
+ clock: this.clock,
131
+ sink: this.sink
132
+ });
133
+ }
134
+ };
135
+ var RecordingActiveSpan = class {
136
+ context;
137
+ name;
138
+ surface;
139
+ kind;
140
+ startTimeMs;
141
+ clock;
142
+ sink;
143
+ events = [];
144
+ attrs;
145
+ ended = false;
146
+ constructor({
147
+ context,
148
+ name,
149
+ surface,
150
+ kind,
151
+ attrs,
152
+ startTimeMs,
153
+ clock,
154
+ sink
155
+ }) {
156
+ this.context = context;
157
+ this.name = name;
158
+ this.surface = surface;
159
+ this.kind = kind;
160
+ this.attrs = attrs;
161
+ this.startTimeMs = startTimeMs;
162
+ this.clock = clock;
163
+ this.sink = sink;
164
+ }
165
+ addEvent(name, attrs) {
166
+ if (this.ended) return;
167
+ this.events.push({
168
+ name,
169
+ timeMs: this.clock(),
170
+ ...attrs ? { attrs } : {}
171
+ });
172
+ }
173
+ end(status = "ok", options = {}) {
174
+ if (this.ended) return;
175
+ this.ended = true;
176
+ const endTimeMs = this.clock();
177
+ const attrs = mergeAttrs(this.attrs, options.attrs);
178
+ this.sink.record({
179
+ context: this.context,
180
+ name: this.name,
181
+ surface: this.surface,
182
+ kind: this.kind,
183
+ status,
184
+ startTimeMs: this.startTimeMs,
185
+ endTimeMs,
186
+ durationMs: Math.max(0, endTimeMs - this.startTimeMs),
187
+ ...attrs ? { attrs } : {},
188
+ events: [...this.events]
189
+ });
190
+ }
191
+ };
192
+ function mergeAttrs(base, extra) {
193
+ if (!base) return extra;
194
+ if (!extra) return base;
195
+ return { ...base, ...extra };
196
+ }
97
197
  function generateTraceId() {
98
198
  return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
99
199
  }
@@ -473,6 +573,9 @@ function summarizeToolInput(toolName, input) {
473
573
  }
474
574
  }
475
575
 
576
+ // ../shared/src/attachmentPreview.ts
577
+ var CSV_PREVIEW_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
578
+
476
579
  // ../shared/src/testing/failpoints.ts
477
580
  var NoopFailpointRegistry = class {
478
581
  get enabled() {
@@ -548,54 +651,6 @@ var RUNTIMES = [
548
651
  { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
549
652
  { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
550
653
  ];
551
- var RUNTIME_MODELS = {
552
- claude: [
553
- { id: "sonnet", label: "Sonnet" },
554
- { id: "opus", label: "Opus" },
555
- { id: "haiku", label: "Haiku" }
556
- ],
557
- codex: [
558
- { id: "gpt-5.5", label: "GPT-5.5" },
559
- { id: "gpt-5.4", label: "GPT-5.4" },
560
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
561
- { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
562
- { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
563
- { id: "gpt-5.2", label: "GPT-5.2" },
564
- { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
565
- { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
566
- { id: "gpt-5-codex", label: "GPT-5 Codex" },
567
- { id: "gpt-5", label: "GPT-5" }
568
- ],
569
- copilot: [
570
- { id: "gpt-5.4", label: "GPT-5.4" },
571
- { id: "gpt-5.2", label: "GPT-5.2" },
572
- { id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
573
- { id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
574
- ],
575
- cursor: [
576
- { id: "composer-2-fast", label: "Composer 2 Fast" },
577
- { id: "composer-2", label: "Composer 2" },
578
- { id: "auto", label: "Auto" }
579
- ],
580
- gemini: [
581
- { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
582
- { id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
583
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
584
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
585
- ],
586
- opencode: [
587
- { id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
588
- { id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
589
- { id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
590
- { id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
591
- { id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
592
- ],
593
- // Kimi CLI resolves model keys from each user's local config, so the safest
594
- // built-in option is to defer to whatever default model the CLI already uses.
595
- kimi: [
596
- { id: "default", label: "Configured Default" }
597
- ]
598
- };
599
654
  var PLAN_CONFIG = {
600
655
  free: {
601
656
  displayName: "Hobby",
@@ -631,14 +686,15 @@ var DISPLAY_PLAN_CONFIG = {
631
686
  };
632
687
 
633
688
  // src/agentProcessManager.ts
634
- import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
689
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
635
690
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
636
691
  import path11 from "path";
637
- import os4 from "os";
692
+ import os5 from "os";
638
693
 
639
694
  // src/drivers/claude.ts
640
695
  import { spawn } from "child_process";
641
- import { writeFileSync as writeFileSync2 } from "fs";
696
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
697
+ import os from "os";
642
698
  import path3 from "path";
643
699
 
644
700
  // src/drivers/cliTransport.ts
@@ -735,8 +791,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
735
791
  16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
736
792
  17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
737
793
  18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
738
- 19. **\`slock reminder list\`** \u2014 List your reminders.
739
- 20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
794
+ 19. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
795
+ 20. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
796
+ 21. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
797
+ 22. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
798
+ 23. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
740
799
 
741
800
  The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
742
801
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -759,22 +818,23 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
759
818
  9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
760
819
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
761
820
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
762
- 12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
821
+ 12. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
763
822
  13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
764
823
  14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
765
824
  15. **${listRemindersCmd}** \u2014 List your reminders.
766
825
  16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
767
826
  const reminderSection = `### Reminders
768
827
 
769
- Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
770
- Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
828
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
829
+ When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
830
+ Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
771
831
  Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
772
832
  const sendingMessagesSection = isCli ? `### Sending messages
773
833
 
774
834
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
775
- - **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
835
+ - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'EOF'\` followed by the message body and \`EOF\`
776
836
  - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
777
- - **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
837
+ - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'EOF'\` followed by the message body and \`EOF\`
778
838
 
779
839
  Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
780
840
  \`\`\`bash
@@ -814,10 +874,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
814
874
  const discoverySection = isCli ? `### Discovering people and channels
815
875
 
816
876
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
817
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
877
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.
878
+ Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
818
879
 
819
880
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
820
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
881
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.
882
+ Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
821
883
  const channelAwarenessSection = isCli ? `### Channel awareness
822
884
 
823
885
  Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
@@ -831,7 +893,7 @@ Each channel has a **name** and optionally a **description** that define its pur
831
893
  - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
832
894
  const readingHistorySection = isCli ? `### Reading history
833
895
 
834
- \`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
896
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
835
897
 
836
898
  To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
837
899
 
@@ -923,7 +985,7 @@ ${readCmd} shows messages in their current state. If a message was later convert
923
985
  - Reuse existing tasks and threads instead of creating duplicates.
924
986
  - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
925
987
  const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
926
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
988
+ let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.
927
989
 
928
990
  ## Who you are
929
991
 
@@ -1031,6 +1093,17 @@ Keep the user informed. They cannot see your internal reasoning, so:
1031
1093
  - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1032
1094
  - When done, summarize the result.
1033
1095
  - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
1096
+ - For long answers where users need the conclusion first but details still matter, put the conclusion and next action outside any collapse, then use sanitized HTML details blocks for optional depth:
1097
+
1098
+ \`\`\`html
1099
+ <details>
1100
+ <summary>Evidence, logs, or edge cases</summary>
1101
+
1102
+ Detailed notes go here.
1103
+ </details>
1104
+ \`\`\`
1105
+
1106
+ Do not hide the main recommendation, blocker, or required action inside \`<details>\`; only fold supporting evidence, logs, alternatives, or extended rationale.
1034
1107
 
1035
1108
  ### Conversation etiquette
1036
1109
 
@@ -1301,6 +1374,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
1301
1374
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1302
1375
  var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1303
1376
  var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
1377
+ var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
1304
1378
  var CLAUDE_DISALLOWED_TOOLS = [
1305
1379
  "EnterPlanMode",
1306
1380
  "ExitPlanMode",
@@ -1326,10 +1400,101 @@ function probeClaude(deps = {}) {
1326
1400
  version: readCommandVersion(command, [], deps) ?? void 0
1327
1401
  };
1328
1402
  }
1403
+ function isRecord(value) {
1404
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1405
+ }
1406
+ function expandClaudeMcpConfigVariables(raw, vars) {
1407
+ let expanded = raw;
1408
+ for (const [name, value] of Object.entries(vars)) {
1409
+ expanded = expanded.replaceAll(`\${${name}}`, value);
1410
+ }
1411
+ return expanded;
1412
+ }
1413
+ function readClaudeMcpServers(configPath, vars = {}) {
1414
+ try {
1415
+ const parsed = JSON.parse(
1416
+ expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
1417
+ );
1418
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
1419
+ return parsed.mcpServers;
1420
+ } catch (err) {
1421
+ logger.warn(
1422
+ `[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
1423
+ );
1424
+ return null;
1425
+ }
1426
+ }
1427
+ function resolveClaudeConfigDir(ctx, home) {
1428
+ const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
1429
+ return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
1430
+ }
1431
+ function collectClaudeMcpConfigFiles(ctx, home) {
1432
+ const files = [];
1433
+ const pushIfFile = (candidate) => {
1434
+ try {
1435
+ if (existsSync2(candidate) && statSync(candidate).isFile()) {
1436
+ files.push({ path: candidate });
1437
+ }
1438
+ } catch {
1439
+ }
1440
+ };
1441
+ pushIfFile(path3.join(home, ".claude.json"));
1442
+ pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
1443
+ const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
1444
+ try {
1445
+ for (const entry of readdirSync(pluginRoot)) {
1446
+ const pluginPath = path3.join(pluginRoot, entry);
1447
+ const configPath = path3.join(pluginPath, ".mcp.json");
1448
+ try {
1449
+ if (existsSync2(configPath) && statSync(configPath).isFile()) {
1450
+ files.push({
1451
+ path: configPath,
1452
+ vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
1453
+ });
1454
+ }
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ } catch {
1459
+ }
1460
+ return files;
1461
+ }
1462
+ function buildClaudeUserMcpServers(ctx, home) {
1463
+ const servers = /* @__PURE__ */ Object.create(null);
1464
+ for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
1465
+ const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
1466
+ if (!mcpServers) continue;
1467
+ for (const [name, server] of Object.entries(mcpServers)) {
1468
+ if (!isRecord(server)) {
1469
+ logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
1470
+ continue;
1471
+ }
1472
+ servers[name] = server;
1473
+ }
1474
+ }
1475
+ return servers;
1476
+ }
1329
1477
  var ClaudeDriver = class {
1330
1478
  id = "claude";
1479
+ lifecycle = {
1480
+ kind: "persistent",
1481
+ stdin: "gated",
1482
+ inFlightWake: "queue"
1483
+ };
1484
+ communication = {
1485
+ chat: "slock_cli",
1486
+ runtimeControl: "mcp_runtime_actions"
1487
+ };
1488
+ session = {
1489
+ recovery: "resume_or_fresh"
1490
+ };
1491
+ model = {
1492
+ detectedModelsVerifiedAs: "launchable",
1493
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
1494
+ };
1331
1495
  supportsStdinNotification = true;
1332
1496
  mcpToolPrefix = "mcp__chat__";
1497
+ usesSlockCliForCommunication = true;
1333
1498
  // Claude Code supports same-turn steering, but raw stdin injection at an
1334
1499
  // arbitrary busy instant can collide with active signed thinking blocks. The
1335
1500
  // daemon therefore gates busy delivery on Claude stream-json boundaries.
@@ -1364,36 +1529,46 @@ var ClaudeDriver = class {
1364
1529
  }
1365
1530
  return args;
1366
1531
  }
1367
- buildRuntimeActionsMcpConfig(ctx) {
1532
+ buildRuntimeActionsMcpServer(ctx) {
1368
1533
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1369
1534
  const command = isTsSource ? "npx" : "node";
1370
1535
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
1536
+ return {
1537
+ command,
1538
+ args: [
1539
+ ...bridgeArgs,
1540
+ "--agent-id",
1541
+ ctx.agentId,
1542
+ "--server-url",
1543
+ ctx.config.serverUrl,
1544
+ "--auth-token",
1545
+ ctx.config.authToken || ctx.daemonApiKey,
1546
+ "--runtime",
1547
+ this.id,
1548
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1549
+ "--runtime-actions-only"
1550
+ ]
1551
+ };
1552
+ }
1553
+ buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
1554
+ const userMcpServers = buildClaudeUserMcpServers(ctx, home);
1555
+ if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
1556
+ logger.warn(
1557
+ `[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
1558
+ );
1559
+ }
1371
1560
  return JSON.stringify({
1372
1561
  mcpServers: {
1373
- chat: {
1374
- command,
1375
- args: [
1376
- ...bridgeArgs,
1377
- "--agent-id",
1378
- ctx.agentId,
1379
- "--server-url",
1380
- ctx.config.serverUrl,
1381
- "--auth-token",
1382
- ctx.config.authToken || ctx.daemonApiKey,
1383
- "--runtime",
1384
- this.id,
1385
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1386
- "--runtime-actions-only"
1387
- ]
1388
- }
1562
+ ...userMcpServers,
1563
+ [SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
1389
1564
  }
1390
1565
  });
1391
1566
  }
1392
- writeClaudeLaunchFiles(ctx, slockDir) {
1567
+ writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
1393
1568
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1394
1569
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1395
1570
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1396
- writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1571
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
1397
1572
  return { systemPromptPath, mcpConfigPath };
1398
1573
  }
1399
1574
  spawn(ctx) {
@@ -1541,8 +1716,8 @@ var ClaudeDriver = class {
1541
1716
 
1542
1717
  // src/drivers/codex.ts
1543
1718
  import { spawn as spawn2, execSync } from "child_process";
1544
- import { existsSync as existsSync2, readFileSync } from "fs";
1545
- import os from "os";
1719
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1720
+ import os2 from "os";
1546
1721
  import path4 from "path";
1547
1722
  function getCodexNotificationErrorMessage(params) {
1548
1723
  const topLevelMessage = params?.message;
@@ -1556,7 +1731,7 @@ function getCodexNotificationErrorMessage(params) {
1556
1731
  return null;
1557
1732
  }
1558
1733
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1559
- const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1734
+ const existsSyncFn = deps.existsSyncFn ?? existsSync3;
1560
1735
  const execSyncFn = deps.execSyncFn ?? execSync;
1561
1736
  const gitDir = path4.join(workingDirectory, ".git");
1562
1737
  if (existsSyncFn(gitDir)) return;
@@ -1603,14 +1778,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1603
1778
  try {
1604
1779
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1605
1780
  const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1606
- if (existsSync2(candidate)) codexEntry = candidate;
1781
+ if (existsSync3(candidate)) codexEntry = candidate;
1607
1782
  } catch {
1608
1783
  }
1609
1784
  if (!codexEntry) {
1610
1785
  try {
1611
1786
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
1612
1787
  const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1613
- if (existsSync2(candidate)) codexEntry = candidate;
1788
+ if (existsSync3(candidate)) codexEntry = candidate;
1614
1789
  } catch {
1615
1790
  }
1616
1791
  }
@@ -1631,8 +1806,25 @@ function joinReasoningText(item) {
1631
1806
  }
1632
1807
  var CodexDriver = class {
1633
1808
  id = "codex";
1809
+ lifecycle = {
1810
+ kind: "persistent",
1811
+ stdin: "direct",
1812
+ inFlightWake: "steer"
1813
+ };
1814
+ communication = {
1815
+ chat: "slock_cli",
1816
+ runtimeControl: "mcp_runtime_actions"
1817
+ };
1818
+ session = {
1819
+ recovery: "resume_or_fresh"
1820
+ };
1821
+ model = {
1822
+ detectedModelsVerifiedAs: "launchable",
1823
+ toLaunchSpec: (modelId) => ({ params: { model: modelId } })
1824
+ };
1634
1825
  supportsStdinNotification = true;
1635
1826
  mcpToolPrefix = "mcp_chat_";
1827
+ usesSlockCliForCommunication = true;
1636
1828
  busyDeliveryMode = "direct";
1637
1829
  supportsNativeStandingPrompt = true;
1638
1830
  probe() {
@@ -2013,12 +2205,12 @@ var CodexDriver = class {
2013
2205
  return detectCodexModels();
2014
2206
  }
2015
2207
  };
2016
- function detectCodexModels(home = os.homedir()) {
2208
+ function detectCodexModels(home = os2.homedir()) {
2017
2209
  const cachePath = path4.join(home, ".codex", "models_cache.json");
2018
2210
  const configPath = path4.join(home, ".codex", "config.toml");
2019
2211
  let models = [];
2020
2212
  try {
2021
- const raw = readFileSync(cachePath, "utf8");
2213
+ const raw = readFileSync2(cachePath, "utf8");
2022
2214
  const parsed = JSON.parse(raw);
2023
2215
  const entries = Array.isArray(parsed?.models) ? parsed.models : [];
2024
2216
  for (const entry of entries) {
@@ -2027,7 +2219,7 @@ function detectCodexModels(home = os.homedir()) {
2027
2219
  if (entry?.visibility && entry.visibility !== "public") continue;
2028
2220
  if (entry?.supported_in_api === false) continue;
2029
2221
  const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
2030
- models.push({ id: slug, label });
2222
+ models.push({ id: slug, label, verified: "launchable" });
2031
2223
  }
2032
2224
  } catch {
2033
2225
  return null;
@@ -2035,7 +2227,7 @@ function detectCodexModels(home = os.homedir()) {
2035
2227
  if (models.length === 0) return null;
2036
2228
  let defaultModel;
2037
2229
  try {
2038
- const raw = readFileSync(configPath, "utf8");
2230
+ const raw = readFileSync2(configPath, "utf8");
2039
2231
  const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
2040
2232
  if (match) defaultModel = match[1];
2041
2233
  } catch {
@@ -2049,6 +2241,23 @@ import path5 from "path";
2049
2241
  import { writeFileSync as writeFileSync3 } from "fs";
2050
2242
  var CopilotDriver = class {
2051
2243
  id = "copilot";
2244
+ lifecycle = {
2245
+ kind: "per_turn",
2246
+ start: "immediate",
2247
+ exit: "natural",
2248
+ inFlightWake: "spawn_new"
2249
+ };
2250
+ communication = {
2251
+ chat: "mcp_chat_bridge",
2252
+ runtimeControl: "mcp_runtime_actions"
2253
+ };
2254
+ session = {
2255
+ recovery: "resume_or_fresh"
2256
+ };
2257
+ model = {
2258
+ detectedModelsVerifiedAs: "launchable",
2259
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2260
+ };
2052
2261
  supportsStdinNotification = false;
2053
2262
  mcpToolPrefix = "";
2054
2263
  busyDeliveryMode = "none";
@@ -2182,17 +2391,34 @@ var CopilotDriver = class {
2182
2391
  };
2183
2392
 
2184
2393
  // src/drivers/cursor.ts
2185
- import { spawn as spawn4 } from "child_process";
2186
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2394
+ import { spawn as spawn4, spawnSync } from "child_process";
2395
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2187
2396
  import path6 from "path";
2188
2397
  var CursorDriver = class {
2189
2398
  id = "cursor";
2399
+ lifecycle = {
2400
+ kind: "per_turn",
2401
+ start: "immediate",
2402
+ exit: "natural",
2403
+ inFlightWake: "spawn_new"
2404
+ };
2405
+ communication = {
2406
+ chat: "mcp_chat_bridge",
2407
+ runtimeControl: "mcp_runtime_actions"
2408
+ };
2409
+ session = {
2410
+ recovery: "resume_or_fresh"
2411
+ };
2412
+ model = {
2413
+ detectedModelsVerifiedAs: "launchable",
2414
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2415
+ };
2190
2416
  supportsStdinNotification = false;
2191
2417
  mcpToolPrefix = "mcp__chat__";
2192
2418
  busyDeliveryMode = "none";
2193
2419
  spawn(ctx) {
2194
2420
  const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
2195
- if (!existsSync3(cursorDir)) {
2421
+ if (!existsSync4(cursorDir)) {
2196
2422
  mkdirSync2(cursorDir, { recursive: true });
2197
2423
  }
2198
2424
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
@@ -2299,49 +2525,89 @@ var CursorDriver = class {
2299
2525
  messageNotificationStyle: "poll"
2300
2526
  });
2301
2527
  }
2528
+ async detectModels() {
2529
+ return detectCursorModels();
2530
+ }
2302
2531
  };
2532
+ function parseCursorModelsOutput(output) {
2533
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
2534
+ const models = [];
2535
+ let defaultModel;
2536
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
2537
+ const line = rawLine.trim();
2538
+ if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
2539
+ if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
2540
+ let modelLine = line;
2541
+ const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
2542
+ const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
2543
+ if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
2544
+ const markerStart = markerMatch?.index ?? modelLine.length;
2545
+ modelLine = modelLine.slice(0, markerStart).trim();
2546
+ }
2547
+ const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
2548
+ if (!match) continue;
2549
+ const id = match[1]?.trim();
2550
+ if (!id || id.startsWith("-")) continue;
2551
+ const label = match[2]?.trim() || id;
2552
+ models.push({ id, label, verified: "launchable" });
2553
+ if (markers.includes("default")) defaultModel = id;
2554
+ }
2555
+ if (models.length === 0) return null;
2556
+ return { models, default: defaultModel };
2557
+ }
2558
+ function detectCursorModels(runCommand = runCursorModelsCommand) {
2559
+ const result = runCommand();
2560
+ if (result.error || result.status !== 0) return null;
2561
+ return parseCursorModelsOutput(String(result.stdout || ""));
2562
+ }
2563
+ function runCursorModelsCommand() {
2564
+ return spawnSync("cursor-agent", ["models"], {
2565
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
2566
+ encoding: "utf8",
2567
+ timeout: 5e3
2568
+ });
2569
+ }
2303
2570
 
2304
2571
  // src/drivers/gemini.ts
2305
2572
  import { spawn as spawn5 } from "child_process";
2306
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2573
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2307
2574
  import path7 from "path";
2308
- function buildGeminiSpawnEnv(ctx) {
2309
- return {
2310
- ...process.env,
2311
- FORCE_COLOR: "0",
2312
- NO_COLOR: "1",
2313
- // Gemini CLI's trusted-workspace gate breaks our managed headless flow
2314
- // unless we explicitly trust the daemon-owned agent workspace.
2315
- GEMINI_CLI_TRUST_WORKSPACE: "true",
2316
- ...ctx.config.envVars || {}
2317
- };
2575
+ function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2576
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
2577
+ if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
2578
+ spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
2579
+ }
2580
+ return spawnEnv;
2318
2581
  }
2319
2582
  var GeminiDriver = class {
2320
2583
  id = "gemini";
2584
+ lifecycle = {
2585
+ kind: "per_turn",
2586
+ start: "immediate",
2587
+ exit: "natural",
2588
+ inFlightWake: "spawn_new"
2589
+ };
2590
+ communication = {
2591
+ chat: "slock_cli",
2592
+ runtimeControl: "mcp_runtime_actions"
2593
+ };
2594
+ session = {
2595
+ recovery: "resume_or_fresh"
2596
+ };
2597
+ model = {
2598
+ detectedModelsVerifiedAs: "suggestion_only",
2599
+ toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
2600
+ };
2321
2601
  supportsStdinNotification = false;
2322
2602
  mcpToolPrefix = "";
2323
2603
  busyDeliveryMode = "none";
2604
+ usesSlockCliForCommunication = true;
2324
2605
  sessionId = null;
2325
2606
  sessionAnnounced = false;
2326
2607
  spawn(ctx) {
2327
2608
  this.sessionId = ctx.config.sessionId || null;
2328
2609
  this.sessionAnnounced = false;
2329
- const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2330
- if (!existsSync4(geminiDir)) {
2331
- mkdirSync3(geminiDir, { recursive: true });
2332
- }
2333
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2334
- const mcpCommand = isTsSource ? "npx" : "node";
2335
- 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];
2336
- const settingsPath = path7.join(geminiDir, "settings.json");
2337
- writeFileSync5(settingsPath, JSON.stringify({
2338
- mcpServers: {
2339
- chat: {
2340
- command: mcpCommand,
2341
- args: mcpArgs
2342
- }
2343
- }
2344
- }), "utf8");
2610
+ this.writeGeminiSettings(ctx);
2345
2611
  const args = [
2346
2612
  "--output-format",
2347
2613
  "stream-json",
@@ -2415,28 +2681,64 @@ var GeminiDriver = class {
2415
2681
  return null;
2416
2682
  }
2417
2683
  buildSystemPrompt(config, _agentId) {
2418
- return buildMcpSystemPrompt(config, {
2684
+ return buildCliTransportSystemPrompt(config, {
2419
2685
  toolPrefix: "",
2420
2686
  extraCriticalRules: [
2421
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
2687
+ "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2688
+ ],
2689
+ postStartupNotes: [
2690
+ "**Gemini runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
2422
2691
  ],
2423
- postStartupNotes: [],
2424
2692
  includeStdinNotificationSection: false,
2425
2693
  messageNotificationStyle: "poll"
2426
2694
  });
2427
2695
  }
2696
+ writeGeminiSettings(ctx) {
2697
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2698
+ mkdirSync3(geminiDir, { recursive: true });
2699
+ const settingsPath = path7.join(geminiDir, "settings.json");
2700
+ writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
2701
+ }
2702
+ buildRuntimeActionsMcpSettings(ctx) {
2703
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2704
+ const command = isTsSource ? "npx" : "node";
2705
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2706
+ return {
2707
+ mcpServers: {
2708
+ chat: {
2709
+ command,
2710
+ args: [
2711
+ ...bridgeArgs,
2712
+ "--agent-id",
2713
+ ctx.agentId,
2714
+ "--server-url",
2715
+ ctx.config.serverUrl,
2716
+ "--auth-token",
2717
+ ctx.config.authToken || ctx.daemonApiKey,
2718
+ "--runtime",
2719
+ this.id,
2720
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2721
+ "--runtime-actions-only"
2722
+ ]
2723
+ }
2724
+ }
2725
+ };
2726
+ }
2428
2727
  };
2429
2728
 
2430
2729
  // src/drivers/kimi.ts
2431
2730
  import { randomUUID } from "crypto";
2432
2731
  import { spawn as spawn6 } from "child_process";
2433
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync6 } from "fs";
2434
- import os2 from "os";
2732
+ import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2733
+ import os3 from "os";
2435
2734
  import path8 from "path";
2436
2735
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2437
2736
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
2438
2737
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
2439
2738
  var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
2739
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
2740
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
2741
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
2440
2742
  function parseToolArguments(raw) {
2441
2743
  if (typeof raw !== "string") return raw;
2442
2744
  try {
@@ -2445,8 +2747,109 @@ function parseToolArguments(raw) {
2445
2747
  return raw;
2446
2748
  }
2447
2749
  }
2750
+ function readKimiConfigSource(home = os3.homedir(), env = process.env) {
2751
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2752
+ if (inlineConfig && inlineConfig.trim()) {
2753
+ return {
2754
+ raw: inlineConfig,
2755
+ explicitPath: null,
2756
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
2757
+ };
2758
+ }
2759
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
2760
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path8.join(home, ".kimi", "config.toml");
2761
+ try {
2762
+ return {
2763
+ raw: readFileSync3(configPath, "utf8"),
2764
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2765
+ sourcePath: configPath
2766
+ };
2767
+ } catch {
2768
+ return {
2769
+ raw: null,
2770
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2771
+ sourcePath: configPath
2772
+ };
2773
+ }
2774
+ }
2775
+ function buildKimiSpawnEnv(env = process.env) {
2776
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
2777
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2778
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
2779
+ return spawnEnv;
2780
+ }
2781
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
2782
+ return {
2783
+ ...process.env,
2784
+ ...ctx.config.envVars || {},
2785
+ ...overrideEnv || {}
2786
+ };
2787
+ }
2788
+ function buildKimiLaunchOptions(ctx, opts = {}) {
2789
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
2790
+ const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
2791
+ const args = [];
2792
+ let configFilePath = null;
2793
+ let configContent = null;
2794
+ if (source.explicitPath) {
2795
+ configFilePath = source.explicitPath;
2796
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
2797
+ configFilePath = path8.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
2798
+ configContent = source.raw;
2799
+ if (opts.writeGeneratedConfig !== false) {
2800
+ writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
2801
+ chmodSync(configFilePath, 384);
2802
+ }
2803
+ }
2804
+ if (configFilePath) {
2805
+ args.push("--config-file", configFilePath);
2806
+ }
2807
+ if (ctx.config.model && ctx.config.model !== "default") {
2808
+ args.push("--model", ctx.config.model);
2809
+ }
2810
+ return {
2811
+ args,
2812
+ env: buildKimiSpawnEnv(env),
2813
+ configFilePath,
2814
+ configContent
2815
+ };
2816
+ }
2448
2817
  var KimiDriver = class {
2449
2818
  id = "kimi";
2819
+ lifecycle = {
2820
+ kind: "persistent",
2821
+ stdin: "direct",
2822
+ inFlightWake: "steer"
2823
+ };
2824
+ communication = {
2825
+ chat: "mcp_chat_bridge",
2826
+ runtimeControl: "mcp_runtime_actions"
2827
+ };
2828
+ session = {
2829
+ recovery: "resume_or_fresh"
2830
+ };
2831
+ model = {
2832
+ detectedModelsVerifiedAs: "launchable",
2833
+ toLaunchSpec: (modelId, ctx, opts) => {
2834
+ if (!ctx) return { args: ["--model", modelId] };
2835
+ const launchCtx = {
2836
+ ...ctx,
2837
+ config: {
2838
+ ...ctx.config,
2839
+ model: modelId
2840
+ }
2841
+ };
2842
+ const launch = buildKimiLaunchOptions(launchCtx, {
2843
+ home: opts?.home,
2844
+ writeGeneratedConfig: false
2845
+ });
2846
+ return {
2847
+ args: launch.args,
2848
+ env: launch.env,
2849
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
2850
+ };
2851
+ }
2852
+ };
2450
2853
  supportsStdinNotification = true;
2451
2854
  mcpToolPrefix = "";
2452
2855
  busyDeliveryMode = "direct";
@@ -2497,6 +2900,7 @@ var KimiDriver = class {
2497
2900
  }
2498
2901
  }
2499
2902
  }), "utf8");
2903
+ const launch = buildKimiLaunchOptions(ctx);
2500
2904
  const args = [
2501
2905
  "--wire",
2502
2906
  "--yolo",
@@ -2505,16 +2909,13 @@ var KimiDriver = class {
2505
2909
  "--mcp-config-file",
2506
2910
  mcpConfigPath,
2507
2911
  "--session",
2508
- this.sessionId
2912
+ this.sessionId,
2913
+ ...launch.args
2509
2914
  ];
2510
- if (ctx.config.model && ctx.config.model !== "default") {
2511
- args.push("--model", ctx.config.model);
2512
- }
2513
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
2514
2915
  const proc = spawn6("kimi", args, {
2515
2916
  cwd: ctx.workingDirectory,
2516
2917
  stdio: ["pipe", "pipe", "pipe"],
2517
- env: spawnEnv,
2918
+ env: launch.env,
2518
2919
  shell: process.platform === "win32"
2519
2920
  });
2520
2921
  proc.stdin?.write(JSON.stringify({
@@ -2631,14 +3032,9 @@ var KimiDriver = class {
2631
3032
  return detectKimiModels();
2632
3033
  }
2633
3034
  };
2634
- function detectKimiModels(home = os2.homedir()) {
2635
- const configPath = path8.join(home, ".kimi", "config.toml");
2636
- let raw;
2637
- try {
2638
- raw = readFileSync2(configPath, "utf8");
2639
- } catch {
2640
- return null;
2641
- }
3035
+ function detectKimiModels(home = os3.homedir(), opts = {}) {
3036
+ const raw = readKimiConfigSource(home, opts.env).raw;
3037
+ if (raw === null) return null;
2642
3038
  const models = [];
2643
3039
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
2644
3040
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -2647,7 +3043,7 @@ function detectKimiModels(home = os2.homedir()) {
2647
3043
  let key = match[1].trim();
2648
3044
  if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
2649
3045
  if (!key) continue;
2650
- models.push({ id: key, label: key });
3046
+ models.push({ id: key, label: key, verified: "launchable" });
2651
3047
  }
2652
3048
  void sectionRe;
2653
3049
  if (models.length === 0) return null;
@@ -2658,9 +3054,9 @@ function detectKimiModels(home = os2.homedir()) {
2658
3054
  }
2659
3055
 
2660
3056
  // src/drivers/opencode.ts
2661
- import { spawn as spawn7 } from "child_process";
2662
- import { readFileSync as readFileSync3 } from "fs";
2663
- import os3 from "os";
3057
+ import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
3058
+ import { readFileSync as readFileSync4 } from "fs";
3059
+ import os4 from "os";
2664
3060
  import path9 from "path";
2665
3061
  var CHAT_MCP_SERVER_NAME = "chat";
2666
3062
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
@@ -2668,6 +3064,14 @@ var SLOCK_AGENT_NAME = "slock";
2668
3064
  var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2669
3065
  var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2670
3066
  var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
3067
+ var OPENCODE_PROVIDER_LABELS = {
3068
+ opencode: "OpenCode",
3069
+ "opencode-go": "OpenCode Go",
3070
+ openai: "OpenAI",
3071
+ openrouter: "OpenRouter",
3072
+ deepseek: "DeepSeek",
3073
+ fusecode: "FuseCode"
3074
+ };
2671
3075
  function buildChatBridgeCommand(ctx) {
2672
3076
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2673
3077
  return [
@@ -2700,10 +3104,10 @@ function parseUserOpenCodeConfig(ctx) {
2700
3104
  const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2701
3105
  return parseOpenCodeConfigContent(raw);
2702
3106
  }
2703
- function readLocalOpenCodeConfig(home = os3.homedir()) {
3107
+ function readLocalOpenCodeConfig(home = os4.homedir()) {
2704
3108
  const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2705
3109
  try {
2706
- return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
3110
+ return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
2707
3111
  } catch {
2708
3112
  }
2709
3113
  return {};
@@ -2749,7 +3153,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
2749
3153
  }
2750
3154
  };
2751
3155
  }
2752
- function buildOpenCodeConfig(ctx, home = os3.homedir()) {
3156
+ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
2753
3157
  const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2754
3158
  const userAgents = recordField(userConfig.agent);
2755
3159
  const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
@@ -2774,7 +3178,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2774
3178
  }
2775
3179
  };
2776
3180
  }
2777
- function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
3181
+ function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
2778
3182
  const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2779
3183
  const config = buildOpenCodeConfig(ctx, home);
2780
3184
  const env = {
@@ -2801,21 +3205,92 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2801
3205
  args.push("--", turnPrompt);
2802
3206
  return { args, env, config };
2803
3207
  }
2804
- function detectOpenCodeModels(home = os3.homedir()) {
2805
- const models = [...RUNTIME_MODELS.opencode || []];
2806
- const providers = recordField(readLocalOpenCodeConfig(home).provider);
2807
- for (const [providerId, providerConfig] of Object.entries(providers)) {
2808
- const providerModels = recordField(recordField(providerConfig).models);
2809
- for (const [modelId, modelConfig] of Object.entries(providerModels)) {
2810
- const fullId = `${providerId}/${modelId}`;
2811
- if (models.some((model2) => model2.id === fullId)) continue;
2812
- const model = recordField(modelConfig);
2813
- const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
2814
- models.push({ id: fullId, label: name });
2815
- }
3208
+ function parseOpenCodeModelsOutput(output) {
3209
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
3210
+ const models = [];
3211
+ const seen = /* @__PURE__ */ new Set();
3212
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
3213
+ const line = rawLine.trim();
3214
+ if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
3215
+ if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
3216
+ if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
3217
+ if (seen.has(line)) continue;
3218
+ seen.add(line);
3219
+ models.push({
3220
+ id: line,
3221
+ label: formatOpenCodeModelLabel(line),
3222
+ verified: "launchable"
3223
+ });
2816
3224
  }
2817
3225
  return models.length > 0 ? { models } : null;
2818
3226
  }
3227
+ function formatOpenCodeModelLabel(modelId) {
3228
+ const separatorIndex = modelId.indexOf("/");
3229
+ if (separatorIndex <= 0 || separatorIndex === modelId.length - 1) return modelId;
3230
+ const providerId = modelId.slice(0, separatorIndex);
3231
+ const modelName = modelId.slice(separatorIndex + 1);
3232
+ const providerLabel = OPENCODE_PROVIDER_LABELS[providerId] || humanizeOpenCodeSegment(providerId);
3233
+ const modelParts = modelName.split("/");
3234
+ const modelLabel = humanizeOpenCodeSegment(modelParts[modelParts.length - 1] || modelName);
3235
+ if (modelParts.length === 1) return `${modelLabel} \xB7 ${providerLabel}`;
3236
+ const upstreamLabel = modelParts.slice(0, -1).map(humanizeOpenCodeSegment).join(" / ");
3237
+ return `${modelLabel} \xB7 ${upstreamLabel} via ${providerLabel}`;
3238
+ }
3239
+ function humanizeOpenCodeSegment(value) {
3240
+ return value.replace(/\[(\d+)m\]/gi, "-$1m").split(/[-_]/).filter(Boolean).map(formatOpenCodeLabelToken).join(" ");
3241
+ }
3242
+ function formatOpenCodeLabelToken(token) {
3243
+ const normalized = token.toLowerCase();
3244
+ const specialCases = {
3245
+ ai: "AI",
3246
+ api: "API",
3247
+ b: "B",
3248
+ chatgpt: "ChatGPT",
3249
+ claude: "Claude",
3250
+ codestral: "Codestral",
3251
+ deepseek: "DeepSeek",
3252
+ flash: "Flash",
3253
+ free: "Free",
3254
+ gemini: "Gemini",
3255
+ glm: "GLM",
3256
+ gpt: "GPT",
3257
+ hy3: "HY3",
3258
+ kimi: "Kimi",
3259
+ m: "M",
3260
+ minimax: "MiniMax",
3261
+ nano: "Nano",
3262
+ nemotron: "Nemotron",
3263
+ omni: "Omni",
3264
+ opus: "Opus",
3265
+ pro: "Pro",
3266
+ sonnet: "Sonnet",
3267
+ super: "Super"
3268
+ };
3269
+ if (specialCases[normalized]) return specialCases[normalized];
3270
+ if (/^v\d+(\.\d+)?$/.test(normalized)) return normalized.toUpperCase();
3271
+ if (/^\d+m$/i.test(token)) return token.toUpperCase();
3272
+ if (/^\d+[bk]$/i.test(token)) return token.toUpperCase();
3273
+ if (/^m\d+(\.\d+)?$/i.test(token)) return token.toUpperCase();
3274
+ if (/^\d/.test(token)) return token;
3275
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
3276
+ }
3277
+ function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3278
+ const commandResult = runCommand(home);
3279
+ if (commandResult.error || commandResult.status !== 0) return null;
3280
+ return parseOpenCodeModelsOutput(commandResult.stdout);
3281
+ }
3282
+ function runOpenCodeModelsCommand(home) {
3283
+ const result = spawnSync2("opencode", ["models"], {
3284
+ env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
3285
+ encoding: "utf8",
3286
+ timeout: 5e3
3287
+ });
3288
+ return {
3289
+ status: result.status,
3290
+ stdout: String(result.stdout || ""),
3291
+ error: result.error
3292
+ };
3293
+ }
2819
3294
  function isSystemFirstMessageTask(message) {
2820
3295
  return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2821
3296
  }
@@ -2834,6 +3309,38 @@ function buildOpenCodeSystemPrompt(config) {
2834
3309
  }
2835
3310
  var OpenCodeDriver = class {
2836
3311
  id = "opencode";
3312
+ lifecycle = {
3313
+ kind: "per_turn",
3314
+ start: "defer_until_concrete_message",
3315
+ exit: "terminate_on_turn_end",
3316
+ inFlightWake: "coalesce_into_pending"
3317
+ };
3318
+ communication = {
3319
+ chat: "slock_cli",
3320
+ runtimeControl: "mcp_runtime_actions"
3321
+ };
3322
+ session = {
3323
+ recovery: "resume_or_fresh"
3324
+ };
3325
+ model = {
3326
+ detectedModelsVerifiedAs: "launchable",
3327
+ toLaunchSpec: (modelId, ctx, opts) => {
3328
+ if (!ctx) return { args: ["--model", modelId] };
3329
+ const launchCtx = {
3330
+ ...ctx,
3331
+ config: {
3332
+ ...ctx.config,
3333
+ model: modelId
3334
+ }
3335
+ };
3336
+ const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
3337
+ return {
3338
+ args: launch.args,
3339
+ env: launch.env,
3340
+ config: launch.config
3341
+ };
3342
+ }
3343
+ };
2837
3344
  supportsStdinNotification = false;
2838
3345
  mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
2839
3346
  busyDeliveryMode = "none";
@@ -3059,7 +3566,53 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3059
3566
  }
3060
3567
 
3061
3568
  // src/agentProcessManager.ts
3062
- var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
3569
+ var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3570
+ var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3571
+ var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3572
+ var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
3573
+ var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
3574
+ var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
3575
+ ".md",
3576
+ ".txt",
3577
+ ".json",
3578
+ ".js",
3579
+ ".ts",
3580
+ ".jsx",
3581
+ ".tsx",
3582
+ ".yaml",
3583
+ ".yml",
3584
+ ".toml",
3585
+ ".log",
3586
+ ".csv",
3587
+ ".xml",
3588
+ ".html",
3589
+ ".css",
3590
+ ".sh",
3591
+ ".py"
3592
+ ]);
3593
+ var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
3594
+ ".apng": "image/apng",
3595
+ ".avif": "image/avif",
3596
+ ".gif": "image/gif",
3597
+ ".jpg": "image/jpeg",
3598
+ ".jpeg": "image/jpeg",
3599
+ ".png": "image/png",
3600
+ ".webp": "image/webp"
3601
+ };
3602
+ function readPositiveIntegerEnv(name, fallback) {
3603
+ const raw = process.env[name];
3604
+ if (!raw) return fallback;
3605
+ const parsed = Number(raw);
3606
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
3607
+ return Math.floor(parsed);
3608
+ }
3609
+ function readNonNegativeIntegerEnv(name, fallback) {
3610
+ const raw = process.env[name];
3611
+ if (!raw) return fallback;
3612
+ const parsed = Number(raw);
3613
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
3614
+ return Math.floor(parsed);
3615
+ }
3063
3616
  function toLocalTime(iso) {
3064
3617
  const d = new Date(iso);
3065
3618
  if (isNaN(d.getTime())) return iso;
@@ -3085,6 +3638,7 @@ function formatMessageTarget(message) {
3085
3638
  function getMessageShortId(messageId) {
3086
3639
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
3087
3640
  }
3641
+ var RESPONSE_TARGET_HINT = "Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.";
3088
3642
  function findSessionJsonl(root, predicate) {
3089
3643
  let visited = 0;
3090
3644
  const maxEntries = 1e4;
@@ -3093,7 +3647,7 @@ function findSessionJsonl(root, predicate) {
3093
3647
  if (depth < 0 || visited >= maxEntries) return null;
3094
3648
  let entries;
3095
3649
  try {
3096
- entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3650
+ entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3097
3651
  } catch {
3098
3652
  return null;
3099
3653
  }
@@ -3139,11 +3693,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3139
3693
  return null;
3140
3694
  }
3141
3695
  }
3142
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3696
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3143
3697
  const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3144
3698
  if (directPath) {
3145
3699
  try {
3146
- if (statSync(directPath).isFile()) {
3700
+ if (statSync2(directPath).isFile()) {
3147
3701
  return { label: sessionId, path: directPath, runtime, reachable: true };
3148
3702
  }
3149
3703
  } catch {
@@ -3239,6 +3793,16 @@ function formatRuntimeProfileControlPrompt(messages) {
3239
3793
  return null;
3240
3794
  }
3241
3795
  const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
3796
+ const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
3797
+ if (!hasMigration) {
3798
+ return [
3799
+ "Runtime Profile daemon release notice.",
3800
+ "",
3801
+ "Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
3802
+ "",
3803
+ body
3804
+ ].join("\n");
3805
+ }
3242
3806
  return [
3243
3807
  "Runtime Profile control notice.",
3244
3808
  "",
@@ -3744,6 +4308,15 @@ function classifyTerminalFailure(ap) {
3744
4308
  }
3745
4309
  return null;
3746
4310
  }
4311
+ function hasDirectStdinRecoveryEvidence(ap) {
4312
+ const candidates = [
4313
+ ap.lastRuntimeError,
4314
+ ...ap.recentStderr
4315
+ ].filter((value) => !!value);
4316
+ return candidates.some(
4317
+ (text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
4318
+ );
4319
+ }
3747
4320
  function isMissingResumeSession(ap) {
3748
4321
  if (!ap.sessionId) return false;
3749
4322
  const candidates = [
@@ -3760,6 +4333,66 @@ function isMissingResumeSession(ap) {
3760
4333
  }
3761
4334
  return false;
3762
4335
  }
4336
+ function classifyActivityDetailForTrace(detail) {
4337
+ if (!detail) return void 0;
4338
+ if (detail === "Message received") return "message_received";
4339
+ if (detail === "Starting\u2026") return "starting";
4340
+ if (detail === "Running command\u2026") return "running_command";
4341
+ if (detail === "Checking messages\u2026") return "checking_messages";
4342
+ if (detail === "Compacting context") return "compacting_context";
4343
+ if (detail === "Context compaction finished") return "compaction_finished";
4344
+ if (detail === "Context compaction still running; no finish event observed") return "compaction_stale";
4345
+ if (detail === "Idle" || detail === "Process idle") return "idle";
4346
+ if (detail.startsWith("Restarting stalled ") && detail.endsWith(" runtime for queued message")) return "stalled_recovery";
4347
+ if (detail.startsWith("Runtime stalled: no runtime events for ")) return "runtime_stalled";
4348
+ return "other";
4349
+ }
4350
+ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
4351
+ const context = [];
4352
+ if (ap.lastActivityDetail) {
4353
+ context.push(`after ${ap.lastActivityDetail}`);
4354
+ }
4355
+ if (ap.driver.busyDeliveryMode === "gated") {
4356
+ context.push(`phase=${ap.gatedSteering.phase}`);
4357
+ }
4358
+ if (ap.gatedSteering.outstandingToolUses > 0) {
4359
+ context.push(`tools=${ap.gatedSteering.outstandingToolUses}`);
4360
+ }
4361
+ if (ap.gatedSteering.compacting) {
4362
+ context.push("compacting");
4363
+ }
4364
+ if (ap.inbox.length > 0) {
4365
+ context.push(`queued=${ap.inbox.length}`);
4366
+ }
4367
+ const detail = [
4368
+ `Runtime stalled: no runtime events for ${staleForMinutes}m`,
4369
+ context.length > 0 ? ` (${context.join(", ")})` : ""
4370
+ ].join("");
4371
+ return {
4372
+ detail,
4373
+ traceAttrs: {
4374
+ ageMs: staleForMs,
4375
+ staleForMinutes,
4376
+ lastActivity: ap.lastActivity,
4377
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
4378
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
4379
+ runtime: ap.config.runtime,
4380
+ model: ap.config.model,
4381
+ launchId: ap.launchId || void 0,
4382
+ sessionIdPresent: Boolean(ap.sessionId),
4383
+ inboxCount: ap.inbox.length,
4384
+ pendingNotificationCount: ap.pendingNotificationCount,
4385
+ processPidPresent: typeof ap.process.pid === "number",
4386
+ busyDeliveryMode: ap.driver.busyDeliveryMode,
4387
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
4388
+ gatedPhase: ap.driver.busyDeliveryMode === "gated" ? ap.gatedSteering.phase : void 0,
4389
+ outstandingToolUses: ap.gatedSteering.outstandingToolUses,
4390
+ compacting: ap.gatedSteering.compacting,
4391
+ recentStderrCount: ap.recentStderr.length,
4392
+ recentStdoutCount: ap.recentStdout.length
4393
+ }
4394
+ };
4395
+ }
3763
4396
  function getMessageDeliveryText(driver) {
3764
4397
  return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
3765
4398
  }
@@ -3783,6 +4416,14 @@ var AgentProcessManager = class _AgentProcessManager {
3783
4416
  agents = /* @__PURE__ */ new Map();
3784
4417
  agentsStarting = /* @__PURE__ */ new Set();
3785
4418
  // Prevent concurrent starts of same agent
4419
+ queuedAgentStarts = /* @__PURE__ */ new Map();
4420
+ agentStartQueue = [];
4421
+ activeAgentStartCount = 0;
4422
+ agentStartPumpTimer = null;
4423
+ lastAgentStartAt = 0;
4424
+ lastAgentStartAgentId = null;
4425
+ maxConcurrentAgentStarts;
4426
+ agentStartIntervalMs;
3786
4427
  startingInboxes = /* @__PURE__ */ new Map();
3787
4428
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
3788
4429
  idleAgentConfigs = /* @__PURE__ */ new Map();
@@ -3796,6 +4437,7 @@ var AgentProcessManager = class _AgentProcessManager {
3796
4437
  driverResolver;
3797
4438
  defaultAgentEnvVarsProvider;
3798
4439
  tracer;
4440
+ deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
3799
4441
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
3800
4442
  this.chatBridgePath = chatBridgePath;
3801
4443
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -3803,21 +4445,248 @@ var AgentProcessManager = class _AgentProcessManager {
3803
4445
  this.daemonApiKey = daemonApiKey;
3804
4446
  this.serverUrl = opts.serverUrl;
3805
4447
  this.dataDir = opts.dataDir || DATA_DIR;
3806
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
4448
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
3807
4449
  this.driverResolver = opts.driverResolver || getDriver;
3808
4450
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3809
4451
  this.tracer = opts.tracer ?? noopTracer;
4452
+ this.maxConcurrentAgentStarts = Math.max(
4453
+ 1,
4454
+ Math.floor(
4455
+ opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
4456
+ )
4457
+ );
4458
+ this.agentStartIntervalMs = Math.max(
4459
+ 0,
4460
+ Math.floor(
4461
+ opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
4462
+ )
4463
+ );
3810
4464
  }
3811
- async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
3812
- if (this.agents.has(agentId)) {
3813
- logger.info(`[Agent ${agentId}] Start ignored (already running)`);
3814
- return;
3815
- }
4465
+ setTracer(tracer) {
4466
+ this.tracer = tracer;
4467
+ }
4468
+ recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
4469
+ const span = this.tracer.startSpan(name, {
4470
+ parent: parseTraceparent(parentTraceparent),
4471
+ surface: "daemon",
4472
+ kind: "internal",
4473
+ attrs
4474
+ });
4475
+ span.end(status);
4476
+ }
4477
+ startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4478
+ return {
4479
+ agentId,
4480
+ launchId,
4481
+ runtime: config.runtime,
4482
+ model: config.model,
4483
+ session_id_present: Boolean(config.sessionId),
4484
+ launch_id_present: Boolean(launchId),
4485
+ wake_message_present: Boolean(wakeMessage),
4486
+ unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
4487
+ resume_prompt_present: Boolean(resumePrompt),
4488
+ queue_depth: this.agentStartQueue.length,
4489
+ active_starts: this.activeAgentStartCount,
4490
+ max_concurrent_starts: this.maxConcurrentAgentStarts,
4491
+ min_start_interval_ms: this.agentStartIntervalMs
4492
+ };
4493
+ }
4494
+ getDeliveryTraceContext(message) {
4495
+ return this.deliveryTraceContexts.get(message) ?? {};
4496
+ }
4497
+ deliveryTraceAttrs(agentId, message, attrs = {}) {
4498
+ const context = this.getDeliveryTraceContext(message);
4499
+ const deliveryCorrelationId = context.deliveryId ?? message.message_id;
4500
+ return {
4501
+ agentId,
4502
+ deliveryId: context.deliveryId,
4503
+ delivery_correlation_id: deliveryCorrelationId,
4504
+ channel_type: message.channel_type,
4505
+ sender_type: message.sender_type,
4506
+ messageId: message.message_id,
4507
+ message_id_present: Boolean(message.message_id),
4508
+ ...attrs
4509
+ };
4510
+ }
4511
+ async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4512
+ this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4513
+ if (this.agents.has(agentId)) {
4514
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4515
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4516
+ reason: "already_running"
4517
+ });
4518
+ logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4519
+ return;
4520
+ }
3816
4521
  if (this.agentsStarting.has(agentId)) {
4522
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4523
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4524
+ reason: "already_starting"
4525
+ });
4526
+ logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4527
+ return;
4528
+ }
4529
+ if (this.queuedAgentStarts.has(agentId)) {
4530
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4531
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4532
+ reason: "already_queued"
4533
+ });
4534
+ logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
4535
+ return;
4536
+ }
4537
+ return new Promise((resolve, reject) => {
4538
+ const item = {
4539
+ agentId,
4540
+ config,
4541
+ wakeMessage,
4542
+ unreadSummary,
4543
+ resumePrompt,
4544
+ launchId,
4545
+ resolve,
4546
+ reject
4547
+ };
4548
+ this.agentStartQueue.push(item);
4549
+ this.queuedAgentStarts.set(agentId, item);
4550
+ this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4551
+ logger.info(
4552
+ `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
4553
+ );
4554
+ this.pumpAgentStartQueue();
4555
+ });
4556
+ }
4557
+ pumpAgentStartQueue() {
4558
+ if (this.agentStartPumpTimer) return;
4559
+ if (this.agentStartQueue.length === 0) return;
4560
+ if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
4561
+ const next = this.agentStartQueue[0];
4562
+ const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
4563
+ const elapsed = Date.now() - this.lastAgentStartAt;
4564
+ const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
4565
+ if (waitMs > 0) {
4566
+ this.recordDaemonTrace("daemon.agent.start.rate_limited", {
4567
+ ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
4568
+ wait_ms: waitMs
4569
+ });
4570
+ this.agentStartPumpTimer = setTimeout(() => {
4571
+ this.agentStartPumpTimer = null;
4572
+ this.pumpAgentStartQueue();
4573
+ }, waitMs);
4574
+ return;
4575
+ }
4576
+ const item = this.agentStartQueue.shift();
4577
+ if (!item) return;
4578
+ if (this.queuedAgentStarts.get(item.agentId) !== item) {
4579
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4580
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4581
+ reason: "stale_queue_item"
4582
+ });
4583
+ this.pumpAgentStartQueue();
4584
+ return;
4585
+ }
4586
+ this.queuedAgentStarts.delete(item.agentId);
4587
+ if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
4588
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4589
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4590
+ reason: "already_running_or_starting"
4591
+ });
4592
+ logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
4593
+ item.resolve();
4594
+ this.pumpAgentStartQueue();
4595
+ return;
4596
+ }
4597
+ this.activeAgentStartCount++;
4598
+ this.lastAgentStartAt = Date.now();
4599
+ this.lastAgentStartAgentId = item.agentId;
4600
+ logger.info(
4601
+ `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
4602
+ );
4603
+ this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
4604
+ this.startAgentNow(
4605
+ item.agentId,
4606
+ item.config,
4607
+ item.wakeMessage,
4608
+ item.unreadSummary,
4609
+ item.resumePrompt,
4610
+ item.launchId
4611
+ ).then(() => {
4612
+ this.releaseAgentStartSlot(item.agentId, "spawn attempted");
4613
+ item.resolve();
4614
+ }, (err) => {
4615
+ this.releaseAgentStartSlot(item.agentId, "start failed");
4616
+ item.reject(err);
4617
+ });
4618
+ }
4619
+ releaseAgentStartSlot(agentId, reason) {
4620
+ if (this.activeAgentStartCount <= 0) return;
4621
+ this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
4622
+ this.recordDaemonTrace("daemon.agent.start.slot_released", {
4623
+ agentId,
4624
+ reason,
4625
+ active_starts: this.activeAgentStartCount,
4626
+ queue_depth: this.agentStartQueue.length,
4627
+ max_concurrent_starts: this.maxConcurrentAgentStarts
4628
+ });
4629
+ logger.info(
4630
+ `[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4631
+ );
4632
+ this.pumpAgentStartQueue();
4633
+ }
4634
+ cancelQueuedAgentStart(agentId, reason) {
4635
+ const item = this.queuedAgentStarts.get(agentId);
4636
+ if (!item) return false;
4637
+ this.queuedAgentStarts.delete(agentId);
4638
+ this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
4639
+ this.startingInboxes.delete(agentId);
4640
+ if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
4641
+ clearTimeout(this.agentStartPumpTimer);
4642
+ this.agentStartPumpTimer = null;
4643
+ }
4644
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4645
+ ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4646
+ reason
4647
+ }, "cancelled");
4648
+ logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
4649
+ item.resolve();
4650
+ return true;
4651
+ }
4652
+ cancelAllQueuedAgentStarts(reason) {
4653
+ for (const item of this.agentStartQueue) {
4654
+ if (this.queuedAgentStarts.get(item.agentId) === item) {
4655
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4656
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4657
+ reason
4658
+ }, "cancelled");
4659
+ logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
4660
+ item.resolve();
4661
+ }
4662
+ }
4663
+ this.agentStartQueue = [];
4664
+ this.queuedAgentStarts.clear();
4665
+ this.startingInboxes.clear();
4666
+ if (this.agentStartPumpTimer) {
4667
+ clearTimeout(this.agentStartPumpTimer);
4668
+ this.agentStartPumpTimer = null;
4669
+ }
4670
+ }
4671
+ async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4672
+ if (this.agents.has(agentId)) {
4673
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4674
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4675
+ reason: "already_running"
4676
+ });
4677
+ logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4678
+ return;
4679
+ }
4680
+ if (this.agentsStarting.has(agentId)) {
4681
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4682
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4683
+ reason: "already_starting"
4684
+ });
3817
4685
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
3818
4686
  return;
3819
4687
  }
3820
4688
  this.agentsStarting.add(agentId);
4689
+ this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
3821
4690
  try {
3822
4691
  const driver = this.driverResolver(config.runtime || "claude");
3823
4692
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -3877,6 +4746,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
3877
4746
  prompt += `
3878
4747
 
3879
4748
  Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
4749
+ ${RESPONSE_TARGET_HINT}
3880
4750
 
3881
4751
  IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
3882
4752
  prompt += getBusyDeliveryNote(driver);
@@ -3909,6 +4779,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3909
4779
  });
3910
4780
  this.sendAgentStatus(agentId, "active", launchId || null);
3911
4781
  this.broadcastActivity(agentId, "online", "Process idle");
4782
+ this.recordDaemonTrace("daemon.agent.spawn.deferred", {
4783
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4784
+ pending_messages_count: pendingMessages.length,
4785
+ reason: "defer_until_concrete_message"
4786
+ });
3912
4787
  logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
3913
4788
  for (const message of pendingMessages) {
3914
4789
  this.deliverMessage(agentId, message);
@@ -3926,6 +4801,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3926
4801
  daemonApiKey: this.daemonApiKey,
3927
4802
  launchId: launchId || null
3928
4803
  });
4804
+ this.recordDaemonTrace("daemon.agent.spawn.created", {
4805
+ ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
4806
+ detached: false,
4807
+ new_session: false,
4808
+ process_pid_present: typeof proc.pid === "number"
4809
+ });
3929
4810
  const agentProcess = {
3930
4811
  process: proc,
3931
4812
  driver,
@@ -3947,6 +4828,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3947
4828
  runtimeTraceSpan: null,
3948
4829
  lastActivity: "",
3949
4830
  lastActivityDetail: "",
4831
+ activityClientSeq: 0,
3950
4832
  recentStdout: [],
3951
4833
  recentStderr: [],
3952
4834
  lastRuntimeError: null,
@@ -3959,7 +4841,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3959
4841
  };
3960
4842
  this.startingInboxes.delete(agentId);
3961
4843
  this.agents.set(agentId, agentProcess);
3962
- this.startRuntimeTrace(agentId, agentProcess, "spawn");
4844
+ this.idleAgentConfigs.set(agentId, {
4845
+ config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
4846
+ sessionId: effectiveConfig.sessionId || null,
4847
+ launchId: launchId || null
4848
+ });
4849
+ this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
3963
4850
  this.agentsStarting.delete(agentId);
3964
4851
  if (config.runtimeProfileControl) {
3965
4852
  this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
@@ -3998,6 +4885,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3998
4885
  proc.on("error", (err) => {
3999
4886
  const current = this.agents.get(agentId);
4000
4887
  if (current) current.spawnError = err.message;
4888
+ this.recordDaemonTrace("daemon.agent.process.error", {
4889
+ agentId,
4890
+ launchId: current?.launchId || void 0,
4891
+ runtime: config.runtime,
4892
+ model: config.model,
4893
+ error_class: err.name || typeof err
4894
+ }, "error");
4001
4895
  logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
4002
4896
  });
4003
4897
  proc.on("exit", (code, signal) => {
@@ -4006,6 +4900,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4006
4900
  current.exitCode = code;
4007
4901
  current.exitSignal = signal;
4008
4902
  }
4903
+ this.recordDaemonTrace("daemon.agent.process.exited", {
4904
+ agentId,
4905
+ launchId: current?.launchId || void 0,
4906
+ runtime: config.runtime,
4907
+ model: config.model,
4908
+ exit_code: code,
4909
+ exit_signal: signal,
4910
+ clean_exit: code === 0,
4911
+ runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4912
+ inbox_count: current?.inbox.length ?? 0,
4913
+ pending_notification_count: current?.pendingNotificationCount ?? 0
4914
+ });
4009
4915
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4010
4916
  });
4011
4917
  proc.on("close", (code, signal) => {
@@ -4069,7 +4975,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4069
4975
  }
4070
4976
  if (processEndedCleanly) {
4071
4977
  let queuedWakeMessage;
4072
- if (!ap.driver.supportsStdinNotification) {
4978
+ if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
4073
4979
  while (ap.inbox.length > 0) {
4074
4980
  const candidate = ap.inbox.shift();
4075
4981
  if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
@@ -4175,7 +5081,68 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4175
5081
  }
4176
5082
  return leftKeys.every((key) => left?.[key] === right?.[key]);
4177
5083
  }
5084
+ enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
5085
+ ap.inbox.push(message);
5086
+ if (ap.driver.supportsStdinNotification && ap.sessionId) {
5087
+ ap.pendingNotificationCount++;
5088
+ if (ap.driver.busyDeliveryMode === "gated") {
5089
+ this.recordGatedSteeringEvent(agentId, ap, "buffer", {
5090
+ reason: "runtime_profile",
5091
+ kind,
5092
+ pendingMessages: ap.inbox.length
5093
+ });
5094
+ } else if (!ap.notificationTimer) {
5095
+ ap.notificationTimer = setTimeout(() => {
5096
+ this.sendStdinNotification(agentId);
5097
+ }, 3e3);
5098
+ }
5099
+ }
5100
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
5101
+ agentId,
5102
+ kind,
5103
+ key_present: Boolean(key),
5104
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5105
+ runtime: ap.config.runtime,
5106
+ session_id_present: Boolean(ap.sessionId),
5107
+ launchId: ap.launchId || void 0,
5108
+ inbox_count: ap.inbox.length,
5109
+ pending_notification_count: ap.pendingNotificationCount,
5110
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
5111
+ supports_stdin_notification: ap.driver.supportsStdinNotification
5112
+ });
5113
+ logger.info(
5114
+ `[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
5115
+ );
5116
+ }
5117
+ queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
5118
+ const pending = this.startingInboxes.get(agentId) || [];
5119
+ pending.push(message);
5120
+ this.startingInboxes.set(agentId, pending);
5121
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5122
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
5123
+ agentId,
5124
+ kind,
5125
+ key_present: Boolean(key),
5126
+ outcome: "queued_during_start",
5127
+ startup_pending: true,
5128
+ starting_inbox_count: pending.length,
5129
+ launchId: queuedStart?.launchId
5130
+ });
5131
+ logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
5132
+ }
5133
+ splitRuntimeProfileControlBatch(messages) {
5134
+ const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
5135
+ if (controlMessages.length === 0 || controlMessages.length === messages.length) {
5136
+ return { nextMessages: messages, deferredMessages: [] };
5137
+ }
5138
+ const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
5139
+ return { nextMessages: controlMessages, deferredMessages };
5140
+ }
5141
+ containsOrdinaryInboxMessage(messages) {
5142
+ return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
5143
+ }
4178
5144
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
5145
+ this.cancelQueuedAgentStart(agentId, "stop requested");
4179
5146
  this.idleAgentConfigs.delete(agentId);
4180
5147
  const ap = this.agents.get(agentId);
4181
5148
  if (!ap) {
@@ -4222,51 +5189,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4222
5189
  });
4223
5190
  }
4224
5191
  }
4225
- deliverMessage(agentId, message) {
5192
+ deliverMessage(agentId, message, traceContext = {}) {
5193
+ if (traceContext.deliveryId) {
5194
+ this.deliveryTraceContexts.set(message, traceContext);
5195
+ }
4226
5196
  const ap = this.agents.get(agentId);
4227
5197
  if (!ap) {
4228
- if (this.agentsStarting.has(agentId)) {
5198
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5199
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4229
5200
  const pending = this.startingInboxes.get(agentId) || [];
4230
5201
  pending.push(message);
4231
5202
  this.startingInboxes.set(agentId, pending);
4232
- return;
5203
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5204
+ outcome: "queued_during_start",
5205
+ accepted: true,
5206
+ process_present: false,
5207
+ startup_pending: true,
5208
+ starting_inbox_count: pending.length,
5209
+ launchId: queuedStart?.launchId
5210
+ }));
5211
+ return true;
4233
5212
  }
4234
5213
  const cached = this.idleAgentConfigs.get(agentId);
4235
5214
  if (cached) {
4236
5215
  const driver = this.driverResolver(cached.config.runtime || "claude");
4237
5216
  if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4238
- return;
5217
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5218
+ outcome: "deferred_wake_message",
5219
+ accepted: true,
5220
+ process_present: false,
5221
+ cached_idle_config_present: true,
5222
+ runtime: cached.config.runtime,
5223
+ session_id_present: Boolean(cached.sessionId),
5224
+ launchId: cached.launchId || void 0
5225
+ }));
5226
+ return true;
4239
5227
  }
4240
5228
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
4241
5229
  this.idleAgentConfigs.delete(agentId);
5230
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5231
+ outcome: "auto_restart_from_idle",
5232
+ accepted: true,
5233
+ process_present: false,
5234
+ cached_idle_config_present: true,
5235
+ runtime: cached.config.runtime,
5236
+ session_id_present: Boolean(cached.sessionId),
5237
+ launchId: cached.launchId || void 0
5238
+ }));
4242
5239
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
4243
5240
  logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
4244
5241
  });
5242
+ return true;
4245
5243
  }
4246
- return;
5244
+ logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
5245
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5246
+ outcome: "rejected_no_process",
5247
+ accepted: false,
5248
+ process_present: false,
5249
+ cached_idle_config_present: false
5250
+ }), "error");
5251
+ this.sendAgentStatus(agentId, "inactive", null);
5252
+ this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
5253
+ return false;
4247
5254
  }
4248
5255
  if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4249
- return;
5256
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5257
+ outcome: "deferred_wake_message",
5258
+ accepted: true,
5259
+ process_present: true,
5260
+ runtime: ap.config.runtime,
5261
+ session_id_present: Boolean(ap.sessionId),
5262
+ launchId: ap.launchId || void 0,
5263
+ is_idle: ap.isIdle,
5264
+ inbox_count: ap.inbox.length
5265
+ }));
5266
+ return true;
4250
5267
  }
4251
5268
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
4252
5269
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4253
5270
  nextMessages.push(message);
4254
5271
  ap.isIdle = false;
4255
- this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
5272
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
4256
5273
  this.broadcastActivity(agentId, "working", "Message received");
4257
- this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
4258
- return;
5274
+ const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
5275
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5276
+ outcome: "stdin_idle_delivery",
5277
+ accepted: true,
5278
+ process_present: true,
5279
+ runtime: ap.config.runtime,
5280
+ session_id_present: true,
5281
+ launchId: ap.launchId || void 0,
5282
+ stdin_delivery_accepted: stdinAccepted,
5283
+ delivered_messages_count: nextMessages.length
5284
+ }));
5285
+ return true;
4259
5286
  }
4260
5287
  ap.inbox.push(message);
4261
- if (!ap.driver.supportsStdinNotification) return;
4262
- if (!ap.sessionId) return;
5288
+ if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
5289
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5290
+ outcome: "queued_stalled_recovery",
5291
+ accepted: true,
5292
+ process_present: true,
5293
+ runtime: ap.config.runtime,
5294
+ session_id_present: Boolean(ap.sessionId),
5295
+ launchId: ap.launchId || void 0,
5296
+ inbox_count: ap.inbox.length
5297
+ }));
5298
+ return true;
5299
+ }
5300
+ if (!ap.driver.supportsStdinNotification) {
5301
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5302
+ outcome: "queued_busy_non_stdin",
5303
+ accepted: true,
5304
+ process_present: true,
5305
+ runtime: ap.config.runtime,
5306
+ session_id_present: Boolean(ap.sessionId),
5307
+ launchId: ap.launchId || void 0,
5308
+ inbox_count: ap.inbox.length
5309
+ }));
5310
+ return true;
5311
+ }
5312
+ if (!ap.sessionId) {
5313
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5314
+ outcome: "queued_before_session",
5315
+ accepted: true,
5316
+ process_present: true,
5317
+ runtime: ap.config.runtime,
5318
+ session_id_present: false,
5319
+ launchId: ap.launchId || void 0,
5320
+ inbox_count: ap.inbox.length
5321
+ }));
5322
+ return true;
5323
+ }
4263
5324
  if (ap.driver.busyDeliveryMode === "gated") {
4264
5325
  ap.pendingNotificationCount++;
4265
5326
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4266
5327
  reason: "busy_message",
4267
5328
  pendingMessages: ap.inbox.length
4268
5329
  });
4269
- return;
5330
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5331
+ outcome: "queued_busy_gated",
5332
+ accepted: true,
5333
+ process_present: true,
5334
+ runtime: ap.config.runtime,
5335
+ session_id_present: true,
5336
+ launchId: ap.launchId || void 0,
5337
+ inbox_count: ap.inbox.length,
5338
+ pending_notification_count: ap.pendingNotificationCount
5339
+ }));
5340
+ return true;
4270
5341
  }
4271
5342
  ap.pendingNotificationCount++;
4272
5343
  if (!ap.notificationTimer) {
@@ -4274,6 +5345,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4274
5345
  this.sendStdinNotification(agentId);
4275
5346
  }, 3e3);
4276
5347
  }
5348
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5349
+ outcome: "queued_busy_notification",
5350
+ accepted: true,
5351
+ process_present: true,
5352
+ runtime: ap.config.runtime,
5353
+ session_id_present: true,
5354
+ inbox_count: ap.inbox.length,
5355
+ pending_notification_count: ap.pendingNotificationCount,
5356
+ notification_timer_present: Boolean(ap.notificationTimer)
5357
+ }));
5358
+ return true;
4277
5359
  }
4278
5360
  async resetWorkspace(agentId) {
4279
5361
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4285,6 +5367,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4285
5367
  }
4286
5368
  }
4287
5369
  async stopAll() {
5370
+ this.cancelAllQueuedAgentStarts("daemon shutdown");
4288
5371
  this.idleAgentConfigs.clear();
4289
5372
  const ids = [...this.agents.keys()];
4290
5373
  await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
@@ -4306,6 +5389,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4306
5389
  getIdleAgentSessionIds() {
4307
5390
  const result = [];
4308
5391
  for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
5392
+ if (this.agents.has(agentId)) continue;
4309
5393
  if (sessionId) result.push({ agentId, sessionId, launchId });
4310
5394
  }
4311
5395
  return result;
@@ -4357,7 +5441,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4357
5441
  }
4358
5442
  return reports;
4359
5443
  }
4360
- deliverRuntimeProfileNotification(agentId, key, kind, content) {
5444
+ deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
5445
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5446
+ parent: parseTraceparent(traceparent),
5447
+ surface: "daemon",
5448
+ kind: "consumer",
5449
+ attrs: {
5450
+ agentId,
5451
+ control_kind: kind,
5452
+ key_present: Boolean(key)
5453
+ }
5454
+ });
4361
5455
  const now = (/* @__PURE__ */ new Date()).toISOString();
4362
5456
  const message = {
4363
5457
  channel_id: "system",
@@ -4368,18 +5462,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4368
5462
  sender_type: "system",
4369
5463
  content,
4370
5464
  timestamp: now,
4371
- message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
5465
+ message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
5466
+ traceparent: formatTraceparent(span.context)
4372
5467
  };
4373
5468
  const ap = this.agents.get(agentId);
5469
+ if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
5470
+ this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
5471
+ span.end("ok", {
5472
+ attrs: {
5473
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5474
+ runtime: ap.config.runtime,
5475
+ launchId: ap.launchId || void 0,
5476
+ session_id_present: Boolean(ap.sessionId),
5477
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5478
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5479
+ }
5480
+ });
5481
+ return true;
5482
+ }
4374
5483
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
4375
5484
  ap.isIdle = false;
4376
5485
  this.startRuntimeTrace(agentId, ap, "runtime-profile");
4377
- this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
4378
- return;
5486
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5487
+ span.end(written ? "ok" : "error", {
5488
+ attrs: {
5489
+ outcome: written ? "stdin_idle" : "stdin_failed",
5490
+ runtime: ap.config.runtime,
5491
+ launchId: ap.launchId || void 0,
5492
+ session_id_present: true,
5493
+ supports_stdin_notification: true,
5494
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5495
+ }
5496
+ });
5497
+ return written;
4379
5498
  }
4380
5499
  if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
4381
- this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
4382
- return;
5500
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
5501
+ span.end(written ? "ok" : "error", {
5502
+ attrs: {
5503
+ outcome: written ? "stdin_busy" : "stdin_failed",
5504
+ runtime: ap.config.runtime,
5505
+ launchId: ap.launchId || void 0,
5506
+ session_id_present: true,
5507
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5508
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5509
+ }
5510
+ });
5511
+ return written;
5512
+ }
5513
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5514
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5515
+ this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
5516
+ span.end("ok", {
5517
+ attrs: {
5518
+ outcome: "queued_during_start",
5519
+ startup_pending: true,
5520
+ launchId: queuedStart?.launchId
5521
+ }
5522
+ });
5523
+ return true;
4383
5524
  }
4384
5525
  const cached = this.idleAgentConfigs.get(agentId);
4385
5526
  if (cached) {
@@ -4389,9 +5530,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4389
5530
  logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
4390
5531
  this.idleAgentConfigs.set(agentId, cached);
4391
5532
  });
4392
- return;
5533
+ span.end("ok", {
5534
+ attrs: {
5535
+ outcome: "restart_queued",
5536
+ runtime: cached.config.runtime,
5537
+ launchId: cached.launchId || void 0,
5538
+ session_id_present: Boolean(cached.sessionId)
5539
+ }
5540
+ });
5541
+ return true;
4393
5542
  }
4394
5543
  logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
5544
+ span.end("ok", { attrs: { outcome: "no_path" } });
5545
+ return false;
4395
5546
  }
4396
5547
  ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
4397
5548
  for (const message of messages) {
@@ -4404,19 +5555,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4404
5555
  type: "agent:runtime_profile:migration:ack",
4405
5556
  agentId,
4406
5557
  migrationKey: notification.key,
4407
- launchId: launchId || void 0
5558
+ launchId: launchId || void 0,
5559
+ traceparent: message.traceparent
4408
5560
  });
4409
5561
  } else {
4410
5562
  this.sendToServer({
4411
5563
  type: "agent:runtime_profile:daemon_release_notice:ack",
4412
5564
  agentId,
4413
5565
  noticeKey: notification.key,
4414
- launchId: launchId || void 0
5566
+ launchId: launchId || void 0,
5567
+ traceparent: message.traceparent
4415
5568
  });
4416
5569
  }
4417
5570
  }
4418
5571
  }
4419
5572
  ackInjectedRuntimeProfileControl(agentId, control, launchId) {
5573
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5574
+ surface: "daemon",
5575
+ kind: "internal",
5576
+ attrs: {
5577
+ agentId,
5578
+ control_kind: control.kind,
5579
+ key_present: Boolean(control.key),
5580
+ launchId: launchId || void 0,
5581
+ source: "agent_config"
5582
+ }
5583
+ });
4420
5584
  const title = runtimeProfileNotificationTitle(control.kind);
4421
5585
  this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
4422
5586
  if (control.kind === "migration") {
@@ -4424,24 +5588,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4424
5588
  type: "agent:runtime_profile:migration:ack",
4425
5589
  agentId,
4426
5590
  migrationKey: control.key,
4427
- launchId: launchId || void 0
5591
+ launchId: launchId || void 0,
5592
+ traceparent: formatTraceparent(span.context)
4428
5593
  });
4429
5594
  } else {
4430
5595
  this.sendToServer({
4431
5596
  type: "agent:runtime_profile:daemon_release_notice:ack",
4432
5597
  agentId,
4433
5598
  noticeKey: control.key,
4434
- launchId: launchId || void 0
5599
+ launchId: launchId || void 0,
5600
+ traceparent: formatTraceparent(span.context)
4435
5601
  });
4436
5602
  }
5603
+ span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
4437
5604
  }
4438
5605
  sendRuntimeProfileWireReport(report) {
5606
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
5607
+ surface: "daemon",
5608
+ kind: "producer",
5609
+ attrs: {
5610
+ agentId: report.agentId,
5611
+ launchId: report.launchId || void 0,
5612
+ runtime: report.facts.runtime,
5613
+ model_present: Boolean(report.facts.model),
5614
+ session_ref_present: Boolean(report.facts.sessionRef),
5615
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
5616
+ }
5617
+ });
4439
5618
  this.sendToServer({
4440
5619
  type: "agent:runtime_profile",
4441
5620
  agentId: report.agentId,
4442
5621
  facts: report.facts,
4443
- launchId: report.launchId || void 0
5622
+ launchId: report.launchId || void 0,
5623
+ traceparent: formatTraceparent(span.context)
4444
5624
  });
5625
+ span.end("ok");
4445
5626
  }
4446
5627
  sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4447
5628
  this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
@@ -4484,32 +5665,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4484
5665
  }
4485
5666
  const info = await stat2(resolved);
4486
5667
  if (info.isDirectory()) throw new Error("Cannot read a directory");
4487
- const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
4488
- ".md",
4489
- ".txt",
4490
- ".json",
4491
- ".js",
4492
- ".ts",
4493
- ".jsx",
4494
- ".tsx",
4495
- ".yaml",
4496
- ".yml",
4497
- ".toml",
4498
- ".log",
4499
- ".csv",
4500
- ".xml",
4501
- ".html",
4502
- ".css",
4503
- ".sh",
4504
- ".py"
4505
- ]);
4506
5668
  const ext = path11.extname(resolved).toLowerCase();
4507
- if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4508
- return { content: null, binary: true };
5669
+ if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5670
+ if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5671
+ const content = await readFile(resolved, "utf-8");
5672
+ return { content, binary: false, size: info.size, encoding: "utf-8" };
5673
+ }
5674
+ const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
5675
+ if (imageMimeType) {
5676
+ if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
5677
+ return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
5678
+ }
5679
+ const content = await readFile(resolved, "base64");
5680
+ return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
4509
5681
  }
4510
- if (info.size > 1048576) throw new Error("File too large");
4511
- const content = await readFile(resolved, "utf-8");
4512
- return { content, binary: false };
5682
+ return { content: null, binary: true, size: info.size };
4513
5683
  }
4514
5684
  // Skill scanning
4515
5685
  // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
@@ -4529,7 +5699,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4529
5699
  async listSkills(agentId, runtimeHint) {
4530
5700
  const agent = this.agents.get(agentId);
4531
5701
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4532
- const home = os4.homedir();
5702
+ const home = os5.homedir();
4533
5703
  const workspaceDir = path11.join(this.dataDir, agentId);
4534
5704
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4535
5705
  const globalResults = await Promise.all(
@@ -4619,13 +5789,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4619
5789
  if (!hasToolStart) {
4620
5790
  entries.push({ kind: "status", activity, detail });
4621
5791
  }
5792
+ if (ap) ap.activityClientSeq += 1;
4622
5793
  this.sendToServer({
4623
5794
  type: "agent:activity",
4624
5795
  agentId,
4625
5796
  activity,
4626
5797
  detail,
4627
5798
  entries,
4628
- launchId: launchIdOverride || ap?.launchId || void 0
5799
+ launchId: launchIdOverride || ap?.launchId || void 0,
5800
+ clientSeq: ap?.activityClientSeq
4629
5801
  });
4630
5802
  if (ap) {
4631
5803
  ap.lastActivity = activity;
@@ -4637,12 +5809,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4637
5809
  this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
4638
5810
  activity: ap.lastActivity
4639
5811
  });
5812
+ ap.activityClientSeq += 1;
4640
5813
  this.sendToServer({
4641
5814
  type: "agent:activity",
4642
5815
  agentId,
4643
5816
  activity: ap.lastActivity,
4644
5817
  detail: ap.lastActivityDetail,
4645
- launchId: launchIdOverride || ap.launchId || void 0
5818
+ launchId: launchIdOverride || ap.launchId || void 0,
5819
+ clientSeq: ap.activityClientSeq
4646
5820
  });
4647
5821
  }, ACTIVITY_HEARTBEAT_MS);
4648
5822
  }
@@ -4654,6 +5828,42 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4654
5828
  }
4655
5829
  }
4656
5830
  }
5831
+ /**
5832
+ * Respond to a server-issued `agent:activity_probe`. Echoes the
5833
+ * agent's current `lastActivity` back through the existing
5834
+ * `agent:activity` upstream channel with the matching `probeId`.
5835
+ *
5836
+ * Why this exists: the server's stale-activity sweep used to
5837
+ * synthesize `online` whenever a transient state went 90s without
5838
+ * an update. That invented state without consulting ground truth
5839
+ * and produced "agent shows green/idle but is actually working" UI
5840
+ * staleness (#engineering:72283cf7 task #340 RCA).
5841
+ *
5842
+ * The new flow: server sends `agent:activity_probe` for stale
5843
+ * agents, daemon replies here with the *real* current activity, and
5844
+ * the server only falls back to synth-online if the probe times out
5845
+ * (5s). The body is intentionally minimal — no entries, no
5846
+ * heartbeat side-effects, no state mutation. We just echo what we
5847
+ * already know.
5848
+ *
5849
+ * If the agent is no longer running locally (`ap` undefined), we
5850
+ * report `offline` so the server stops believing the agent is busy.
5851
+ */
5852
+ respondToActivityProbe(agentId, probeId) {
5853
+ const ap = this.agents.get(agentId);
5854
+ const activity = ap?.lastActivity || "offline";
5855
+ const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
5856
+ if (ap) ap.activityClientSeq += 1;
5857
+ this.sendToServer({
5858
+ type: "agent:activity",
5859
+ agentId,
5860
+ activity,
5861
+ detail,
5862
+ launchId: ap?.launchId || void 0,
5863
+ probeId,
5864
+ clientSeq: ap?.activityClientSeq
5865
+ });
5866
+ }
4657
5867
  flushPendingTrajectory(agentId) {
4658
5868
  const ap = this.agents.get(agentId);
4659
5869
  const pending = ap?.pendingTrajectory;
@@ -4719,7 +5929,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4719
5929
  this.clearCompactionWatchdog(ap);
4720
5930
  this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
4721
5931
  }
4722
- startRuntimeTrace(agentId, ap, reason) {
5932
+ messagesTraceAttrs(messages) {
5933
+ if (!messages || messages.length === 0) return {};
5934
+ const first = messages[0];
5935
+ const context = this.getDeliveryTraceContext(first);
5936
+ const notification = runtimeProfileNotificationFromMessage(first);
5937
+ if (notification) {
5938
+ return {
5939
+ messages_count: messages.length,
5940
+ message_id_present: Boolean(first.message_id),
5941
+ deliveryId: context.deliveryId,
5942
+ delivery_correlation_id: context.deliveryId,
5943
+ control_kind: notification.kind,
5944
+ key_present: Boolean(notification.key)
5945
+ };
5946
+ }
5947
+ return {
5948
+ messages_count: messages.length,
5949
+ messageId: first.message_id,
5950
+ message_id_present: Boolean(first.message_id),
5951
+ deliveryId: context.deliveryId,
5952
+ delivery_correlation_id: context.deliveryId ?? first.message_id
5953
+ };
5954
+ }
5955
+ startRuntimeTrace(agentId, ap, reason, messages) {
4723
5956
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
4724
5957
  const span = this.tracer.startSpan("daemon.runtime.turn", {
4725
5958
  surface: "daemon",
@@ -4729,10 +5962,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4729
5962
  runtime: ap.config.runtime,
4730
5963
  model: ap.config.model,
4731
5964
  reason,
4732
- hasSession: Boolean(ap.sessionId)
5965
+ hasSession: Boolean(ap.sessionId),
5966
+ ...this.messagesTraceAttrs(messages)
4733
5967
  }
4734
5968
  });
4735
- span.addEvent("daemon.turn.started", { reason });
5969
+ span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
4736
5970
  ap.runtimeTraceSpan = span;
4737
5971
  return span;
4738
5972
  }
@@ -4831,17 +6065,59 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4831
6065
  if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
4832
6066
  ap.runtimeProgressStaleSince = Date.now();
4833
6067
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
4834
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
6068
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
6069
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", diagnostic.traceAttrs);
6070
+ this.endRuntimeTrace(ap, "error", {
6071
+ outcome: "runtime-stalled",
4835
6072
  ageMs: staleForMs,
4836
- staleForMinutes,
4837
- lastActivity: ap.lastActivity
6073
+ lastActivity: ap.lastActivity,
6074
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6075
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
6076
+ });
6077
+ this.broadcastActivity(agentId, "error", diagnostic.detail);
6078
+ return true;
6079
+ }
6080
+ recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
6081
+ if (ap.inbox.length === 0) return false;
6082
+ if (ap.expectedTerminationReason === "stalled_recovery") {
6083
+ return true;
6084
+ }
6085
+ const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
6086
+ const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
6087
+ const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
6088
+ if (!canRestartStalledProcess) return false;
6089
+ const staleForMs = Date.now() - ap.lastRuntimeEventAt;
6090
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
6091
+ const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
6092
+ ap.runtimeProgressStaleSince ??= Date.now();
6093
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
6094
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
6095
+ ...diagnostic.traceAttrs,
6096
+ pendingMessages: ap.inbox.length,
6097
+ recovery: "terminate_for_queued_message"
4838
6098
  });
4839
6099
  this.endRuntimeTrace(ap, "error", {
4840
6100
  outcome: "runtime-stalled",
4841
6101
  ageMs: staleForMs,
4842
- lastActivity: ap.lastActivity
6102
+ lastActivity: ap.lastActivity,
6103
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6104
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6105
+ pendingMessages: ap.inbox.length,
6106
+ recovery: "terminate_for_queued_message"
4843
6107
  });
4844
- this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
6108
+ ap.expectedTerminationReason = "stalled_recovery";
6109
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
6110
+ logger.warn(
6111
+ `[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
6112
+ );
6113
+ this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
6114
+ try {
6115
+ ap.process.kill("SIGTERM");
6116
+ } catch (err) {
6117
+ const reason = err instanceof Error ? err.message : String(err);
6118
+ logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
6119
+ return false;
6120
+ }
4845
6121
  return true;
4846
6122
  }
4847
6123
  /** Handle a single ParsedEvent from any runtime driver */
@@ -4967,7 +6243,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4967
6243
  }
4968
6244
  } else {
4969
6245
  ap.isIdle = true;
4970
- this.broadcastActivity(agentId, "online", "Idle");
6246
+ if (ap.lastRuntimeError) {
6247
+ this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
6248
+ } else {
6249
+ this.broadcastActivity(agentId, "online", "Idle");
6250
+ }
4971
6251
  }
4972
6252
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
4973
6253
  if (ap.driver.terminateProcessOnTurnEnd) {
@@ -5006,6 +6286,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5006
6286
  }
5007
6287
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
5008
6288
  this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6289
+ if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6290
+ ap.isIdle = true;
6291
+ ap.pendingNotificationCount = 0;
6292
+ if (ap.notificationTimer) {
6293
+ clearTimeout(ap.notificationTimer);
6294
+ ap.notificationTimer = null;
6295
+ }
6296
+ logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
6297
+ }
5009
6298
  }
5010
6299
  this.broadcastActivity(agentId, "error", event.message, [
5011
6300
  { kind: "text", text: `Error: ${event.message}` }
@@ -5052,20 +6341,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5052
6341
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
5053
6342
  if (encoded) {
5054
6343
  ap.process.stdin?.write(encoded + "\n");
6344
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6345
+ agentId,
6346
+ runtime: ap.config.runtime,
6347
+ model: ap.config.model,
6348
+ launchId: ap.launchId || void 0,
6349
+ outcome: "written",
6350
+ mode: "busy",
6351
+ pending_notification_count: count,
6352
+ session_id_present: true
6353
+ });
6354
+ } else {
6355
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6356
+ agentId,
6357
+ runtime: ap.config.runtime,
6358
+ model: ap.config.model,
6359
+ launchId: ap.launchId || void 0,
6360
+ outcome: "encode_failed",
6361
+ mode: "busy",
6362
+ pending_notification_count: count,
6363
+ session_id_present: true
6364
+ }, "error");
5055
6365
  }
5056
6366
  }
5057
6367
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
5058
6368
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
5059
6369
  if (messages.length === 0) return true;
6370
+ const split = this.splitRuntimeProfileControlBatch(messages);
6371
+ if (split.deferredMessages.length > 0) {
6372
+ ap.inbox.unshift(...split.deferredMessages);
6373
+ ap.pendingNotificationCount += split.deferredMessages.length;
6374
+ messages = split.nextMessages;
6375
+ this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
6376
+ agentId,
6377
+ launchId: ap.launchId || void 0,
6378
+ runtime: ap.config.runtime,
6379
+ mode,
6380
+ delivered_control_messages_count: messages.length,
6381
+ deferred_messages_count: split.deferredMessages.length,
6382
+ inbox_count: ap.inbox.length,
6383
+ pending_notification_count: ap.pendingNotificationCount
6384
+ });
6385
+ }
6386
+ const traceAttrs = {
6387
+ agentId,
6388
+ launchId: ap.launchId || void 0,
6389
+ runtime: ap.config.runtime,
6390
+ model: ap.config.model,
6391
+ mode,
6392
+ messages_count: messages.length,
6393
+ session_id_present: Boolean(ap.sessionId),
6394
+ inbox_count: ap.inbox.length,
6395
+ pending_notification_count: ap.pendingNotificationCount,
6396
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
6397
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
6398
+ ...this.messagesTraceAttrs(messages)
6399
+ };
5060
6400
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
5061
6401
 
5062
6402
  ${formatIncomingMessage(messages[0], ap.driver)}
5063
6403
 
5064
- Respond as appropriate. Complete all your work before stopping.` : `New messages received:
6404
+ Respond as appropriate. Complete all your work before stopping.
6405
+ ${RESPONSE_TARGET_HINT}` : `New messages received:
5065
6406
 
5066
6407
  ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
5067
6408
 
5068
- Respond as appropriate. Complete all your work before stopping.`);
6409
+ Respond as appropriate. Complete all your work before stopping.
6410
+ ${RESPONSE_TARGET_HINT}`);
5069
6411
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
5070
6412
  if (!encoded) {
5071
6413
  ap.inbox.unshift(...messages);
@@ -5075,14 +6417,27 @@ Respond as appropriate. Complete all your work before stopping.`);
5075
6417
  logger.warn(
5076
6418
  `[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
5077
6419
  );
6420
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6421
+ ...traceAttrs,
6422
+ outcome: "encode_failed",
6423
+ requeued_messages_count: messages.length
6424
+ }, "error");
5078
6425
  return false;
5079
6426
  }
5080
6427
  const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
5081
6428
  logger.info(
5082
6429
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
5083
6430
  );
6431
+ if (this.containsOrdinaryInboxMessage(messages)) {
6432
+ ap.lastRuntimeError = null;
6433
+ }
5084
6434
  ap.process.stdin?.write(encoded + "\n");
5085
6435
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
6436
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6437
+ ...traceAttrs,
6438
+ outcome: "written",
6439
+ stdin_write_attempted: true
6440
+ });
5086
6441
  return true;
5087
6442
  }
5088
6443
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -5127,6 +6482,16 @@ var systemClock = {
5127
6482
  clearTimeout: (timer) => clearTimeout(timer)
5128
6483
  };
5129
6484
  var INBOUND_WATCHDOG_MS = 7e4;
6485
+ function durationMsBucket(ms) {
6486
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
6487
+ if (ms === 0) return "0";
6488
+ if (ms <= 1e3) return "1s";
6489
+ if (ms <= 1e4) return "1s-10s";
6490
+ if (ms <= 3e4) return "10s-30s";
6491
+ if (ms <= 6e4) return "30s-60s";
6492
+ if (ms <= 12e4) return "60s-120s";
6493
+ return "120s+";
6494
+ }
5130
6495
  var DaemonConnection = class {
5131
6496
  ws = null;
5132
6497
  options;
@@ -5138,6 +6503,8 @@ var DaemonConnection = class {
5138
6503
  shouldConnect = true;
5139
6504
  reconnectAttempt = 0;
5140
6505
  lastDroppedSendLogAt = 0;
6506
+ lastInboundAt = null;
6507
+ lastInboundMessageKind = null;
5141
6508
  constructor(options) {
5142
6509
  this.options = options;
5143
6510
  this.clock = options.clock ?? systemClock;
@@ -5172,6 +6539,10 @@ var DaemonConnection = class {
5172
6539
  this.lastDroppedSendLogAt = now;
5173
6540
  logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
5174
6541
  }
6542
+ this.trace("daemon.connection.outbound_dropped", {
6543
+ message_type: msg.type,
6544
+ ws_ready_state: this.ws?.readyState ?? null
6545
+ });
5175
6546
  }
5176
6547
  get connected() {
5177
6548
  return this.ws?.readyState === WebSocket.OPEN;
@@ -5185,25 +6556,49 @@ var DaemonConnection = class {
5185
6556
  if (wsOptions?.agent) {
5186
6557
  logger.info("[Daemon] Using configured proxy for WebSocket connection");
5187
6558
  }
6559
+ this.trace("daemon.connection.connecting", {
6560
+ reconnect_attempt: this.reconnectAttempt,
6561
+ server_url_present: Boolean(this.options.serverUrl),
6562
+ proxy_present: Boolean(wsOptions?.agent)
6563
+ });
5188
6564
  const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
5189
6565
  this.ws = ws;
5190
6566
  ws.on("open", () => {
5191
6567
  if (this.ws !== ws) return;
5192
6568
  if (!this.shouldConnect) return;
5193
6569
  logger.info("[Daemon] Connected to server");
6570
+ const priorReconnectAttempt = this.reconnectAttempt;
5194
6571
  this.reconnectAttempt = 0;
5195
6572
  this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
6573
+ this.markInbound("websocket_open");
5196
6574
  this.resetWatchdog();
6575
+ this.trace("daemon.connection.connected", {
6576
+ reconnect_attempt: priorReconnectAttempt,
6577
+ inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
6578
+ });
5197
6579
  this.options.onConnect();
5198
6580
  });
5199
6581
  ws.on("message", (data) => {
5200
6582
  if (this.ws !== ws) return;
5201
- this.resetWatchdog();
6583
+ let messageKind = "unknown";
5202
6584
  try {
5203
6585
  const msg = JSON.parse(data.toString());
6586
+ messageKind = msg.type;
6587
+ this.markInbound(messageKind);
6588
+ this.resetWatchdog();
6589
+ this.trace("daemon.connection.inbound_received", {
6590
+ message_type: messageKind,
6591
+ last_inbound_age_ms_bucket: "0"
6592
+ });
5204
6593
  this.options.onMessage(msg);
5205
6594
  } catch (err) {
6595
+ this.markInbound("invalid_json");
6596
+ this.resetWatchdog();
5206
6597
  logger.error("[Daemon] Invalid message from server", err);
6598
+ this.trace("daemon.connection.invalid_message", {
6599
+ error_class: err instanceof Error ? err.name : typeof err,
6600
+ last_inbound_message_kind: "invalid_json"
6601
+ }, "error");
5207
6602
  }
5208
6603
  });
5209
6604
  ws.on("close", (code, reasonBuffer) => {
@@ -5214,12 +6609,23 @@ var DaemonConnection = class {
5214
6609
  logger.warn(
5215
6610
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
5216
6611
  );
6612
+ this.trace("daemon.connection.disconnected", {
6613
+ close_code: code,
6614
+ close_reason_present: Boolean(reason),
6615
+ reconnecting: this.shouldConnect,
6616
+ reconnect_attempt: this.reconnectAttempt,
6617
+ last_inbound_message_kind: this.lastInboundMessageKind,
6618
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
6619
+ }, this.shouldConnect ? "cancelled" : "ok");
5217
6620
  this.options.onDisconnect();
5218
6621
  this.scheduleReconnect();
5219
6622
  });
5220
6623
  ws.on("error", (err) => {
5221
6624
  if (this.ws !== ws) return;
5222
6625
  logger.error(`[Daemon] WebSocket error: ${err.message}`);
6626
+ this.trace("daemon.connection.error", {
6627
+ error_class: err.name || "Error"
6628
+ }, "error");
5223
6629
  });
5224
6630
  }
5225
6631
  scheduleReconnect() {
@@ -5227,6 +6633,10 @@ var DaemonConnection = class {
5227
6633
  if (this.reconnectTimer) return;
5228
6634
  this.reconnectAttempt += 1;
5229
6635
  logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
6636
+ this.trace("daemon.connection.reconnect_scheduled", {
6637
+ reconnect_attempt: this.reconnectAttempt,
6638
+ delay_ms: this.reconnectDelay
6639
+ });
5230
6640
  this.reconnectTimer = this.clock.setTimeout(() => {
5231
6641
  this.reconnectTimer = null;
5232
6642
  this.doConnect();
@@ -5238,6 +6648,13 @@ var DaemonConnection = class {
5238
6648
  const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
5239
6649
  this.watchdogTimer = this.clock.setTimeout(() => {
5240
6650
  logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
6651
+ this.trace("daemon.connection.watchdog_timeout", {
6652
+ inbound_watchdog_ms: ms,
6653
+ last_inbound_message_kind: this.lastInboundMessageKind,
6654
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
6655
+ ws_ready_state: this.ws?.readyState ?? null,
6656
+ reconnecting: this.shouldConnect
6657
+ }, "error");
5241
6658
  try {
5242
6659
  this.ws?.terminate();
5243
6660
  } catch {
@@ -5250,6 +6667,16 @@ var DaemonConnection = class {
5250
6667
  this.watchdogTimer = null;
5251
6668
  }
5252
6669
  }
6670
+ markInbound(messageKind) {
6671
+ this.lastInboundAt = this.clock.now();
6672
+ this.lastInboundMessageKind = messageKind;
6673
+ }
6674
+ lastInboundAgeBucket() {
6675
+ return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
6676
+ }
6677
+ trace(name, attrs, status = "ok") {
6678
+ this.options.onTraceEvent?.(name, attrs, status);
6679
+ }
5253
6680
  };
5254
6681
 
5255
6682
  // src/reminderCache.ts
@@ -5333,10 +6760,10 @@ var ReminderCache = class {
5333
6760
 
5334
6761
  // src/machineLock.ts
5335
6762
  import { createHash, randomUUID as randomUUID2 } from "crypto";
5336
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
5337
- import os5 from "os";
6763
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
6764
+ import os6 from "os";
5338
6765
  import path12 from "path";
5339
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
6766
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
5340
6767
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
5341
6768
  var DaemonMachineLockConflictError = class extends Error {
5342
6769
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -5359,14 +6786,14 @@ function ownerPath(lockDir) {
5359
6786
  }
5360
6787
  function readOwner(lockDir) {
5361
6788
  try {
5362
- return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
6789
+ return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
5363
6790
  } catch {
5364
6791
  return null;
5365
6792
  }
5366
6793
  }
5367
6794
  function lockAgeMs(lockDir) {
5368
6795
  try {
5369
- return Date.now() - statSync2(lockDir).mtimeMs;
6796
+ return Date.now() - statSync3(lockDir).mtimeMs;
5370
6797
  } catch {
5371
6798
  return null;
5372
6799
  }
@@ -5395,7 +6822,7 @@ function acquireDaemonMachineLock(options) {
5395
6822
  const owner = {
5396
6823
  pid: process.pid,
5397
6824
  token,
5398
- hostname: os5.hostname(),
6825
+ hostname: os6.hostname(),
5399
6826
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5400
6827
  serverUrl: options.serverUrl,
5401
6828
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5437,6 +6864,418 @@ function acquireDaemonMachineLock(options) {
5437
6864
  throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
5438
6865
  }
5439
6866
 
6867
+ // src/localTraceSink.ts
6868
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6869
+ import path13 from "path";
6870
+ var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6871
+ var DEFAULT_MAX_FILES = 8;
6872
+ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6873
+ "serverId",
6874
+ "machineId",
6875
+ "agentId",
6876
+ "messageId",
6877
+ "launchId",
6878
+ "uploadId",
6879
+ "bundleId",
6880
+ "deliveryId",
6881
+ "deliveryCorrelationId",
6882
+ "delivery_correlation_id"
6883
+ ]);
6884
+ var LocalRotatingTraceSink = class {
6885
+ traceDir;
6886
+ maxFileBytes;
6887
+ maxFiles;
6888
+ currentFile = null;
6889
+ currentSize = 0;
6890
+ sequence = 0;
6891
+ constructor(options) {
6892
+ this.traceDir = path13.join(options.machineDir, "traces");
6893
+ this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6894
+ this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
6895
+ }
6896
+ record(span) {
6897
+ try {
6898
+ const line = `${JSON.stringify(toLocalTraceRecord(span))}
6899
+ `;
6900
+ this.ensureFile(Buffer.byteLength(line));
6901
+ appendFileSync(this.currentFile, line, { encoding: "utf8" });
6902
+ this.currentSize += Buffer.byteLength(line);
6903
+ } catch {
6904
+ }
6905
+ }
6906
+ getCurrentFile() {
6907
+ return this.currentFile;
6908
+ }
6909
+ ensureFile(nextBytes) {
6910
+ mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
6911
+ if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
6912
+ this.currentFile = path13.join(
6913
+ this.traceDir,
6914
+ `daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6915
+ );
6916
+ writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
6917
+ this.currentSize = statSync4(this.currentFile).size;
6918
+ this.pruneOldFiles();
6919
+ }
6920
+ }
6921
+ pruneOldFiles() {
6922
+ const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
6923
+ const excess = files.length - this.maxFiles;
6924
+ if (excess <= 0) return;
6925
+ for (const file of files.slice(0, excess)) {
6926
+ rmSync3(path13.join(this.traceDir, file), { force: true });
6927
+ }
6928
+ }
6929
+ };
6930
+ function safeTimestamp(timeMs) {
6931
+ return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
6932
+ }
6933
+ function toLocalTraceRecord(span) {
6934
+ return {
6935
+ type: "span",
6936
+ schema_version: 1,
6937
+ trace_id: span.context.traceId,
6938
+ span_id: span.context.spanId,
6939
+ parent_span_id: span.context.parentSpanId,
6940
+ name: span.name,
6941
+ surface: span.surface,
6942
+ kind: span.kind,
6943
+ status: span.status,
6944
+ start_time: new Date(span.startTimeMs).toISOString(),
6945
+ end_time: new Date(span.endTimeMs).toISOString(),
6946
+ duration_ms: span.durationMs,
6947
+ attrs: sanitizeAttrs(span.attrs),
6948
+ events: span.events.map(sanitizeEvent)
6949
+ };
6950
+ }
6951
+ function sanitizeEvent(event) {
6952
+ return {
6953
+ name: event.name,
6954
+ time: new Date(event.timeMs).toISOString(),
6955
+ attrs: sanitizeAttrs(event.attrs)
6956
+ };
6957
+ }
6958
+ function sanitizeAttrs(attrs) {
6959
+ if (!attrs) return void 0;
6960
+ const sanitized = {};
6961
+ for (const [key, value] of Object.entries(attrs)) {
6962
+ if (isDiagnosticIdAttr(key)) {
6963
+ if (value === null || value === void 0 || value === "") continue;
6964
+ sanitized[key] = sanitizeValue(value);
6965
+ continue;
6966
+ }
6967
+ if (shouldDropAttr(key)) continue;
6968
+ sanitized[key] = sanitizeValue(value);
6969
+ }
6970
+ return sanitized;
6971
+ }
6972
+ function sanitizeValue(value) {
6973
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6974
+ return value;
6975
+ }
6976
+ if (Array.isArray(value)) {
6977
+ return { items_count: value.length };
6978
+ }
6979
+ if (typeof value === "object") {
6980
+ return { object_present: true };
6981
+ }
6982
+ return String(value);
6983
+ }
6984
+ function shouldDropAttr(key) {
6985
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
6986
+ if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
6987
+ return true;
6988
+ }
6989
+ if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
6990
+ return false;
6991
+ }
6992
+ if (/(^|_)id$/.test(normalized)) {
6993
+ return true;
6994
+ }
6995
+ return /(^|_)(prompt|content|text|message|body|request|response|command|argv|env|cwd|path|file|error|tool_args|tool_input|tool_output|stdout|stderr)(_|$)/i.test(normalized);
6996
+ }
6997
+ function isDiagnosticIdAttr(key) {
6998
+ return DIAGNOSTIC_ID_ATTRS.has(key);
6999
+ }
7000
+
7001
+ // src/traceBundleUpload.ts
7002
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7003
+ import { gzipSync } from "zlib";
7004
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
7005
+ import path14 from "path";
7006
+
7007
+ // src/directUploadCapability.ts
7008
+ function joinUrl(base, path16) {
7009
+ return `${base.replace(/\/+$/, "")}${path16}`;
7010
+ }
7011
+ function jsonHeaders(apiKey) {
7012
+ return {
7013
+ "Content-Type": "application/json",
7014
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
7015
+ };
7016
+ }
7017
+ async function requestDaemonScopeAttestation({
7018
+ serverUrl,
7019
+ apiKey,
7020
+ scope,
7021
+ metadata,
7022
+ fetchImpl = fetch,
7023
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7024
+ }) {
7025
+ const { response, data } = await executeJsonRequest(
7026
+ joinUrl(serverUrl, "/internal/machine/scope-attestation"),
7027
+ {
7028
+ method: "POST",
7029
+ headers: jsonHeaders(apiKey),
7030
+ body: JSON.stringify({
7031
+ scope,
7032
+ ...metadata ? { metadata } : {}
7033
+ })
7034
+ },
7035
+ {
7036
+ toolName: "daemon_direct_upload.scope_attestation",
7037
+ target: scope,
7038
+ timeoutMs,
7039
+ fetchImpl
7040
+ }
7041
+ );
7042
+ if (!response.ok) {
7043
+ throw new Error(`Failed to request daemon scope attestation (${response.status})`);
7044
+ }
7045
+ return data;
7046
+ }
7047
+ async function createDirectUploadSession({
7048
+ serverUrl,
7049
+ apiKey,
7050
+ workerUrl,
7051
+ scope,
7052
+ createPath = "/api/uploads",
7053
+ body,
7054
+ attestationMetadata,
7055
+ fetchImpl = fetch,
7056
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7057
+ }) {
7058
+ const capability = await requestDaemonScopeAttestation({
7059
+ serverUrl,
7060
+ apiKey,
7061
+ scope,
7062
+ metadata: attestationMetadata,
7063
+ fetchImpl,
7064
+ timeoutMs
7065
+ });
7066
+ const { response, data } = await executeJsonRequest(
7067
+ joinUrl(workerUrl, createPath),
7068
+ {
7069
+ method: "POST",
7070
+ headers: jsonHeaders(),
7071
+ body: JSON.stringify({
7072
+ ...body,
7073
+ attestation: capability.attestation
7074
+ })
7075
+ },
7076
+ {
7077
+ toolName: "daemon_direct_upload.create",
7078
+ target: capability.audience,
7079
+ timeoutMs,
7080
+ fetchImpl
7081
+ }
7082
+ );
7083
+ if (!response.ok) {
7084
+ throw new Error(`Failed to create direct upload session (${response.status})`);
7085
+ }
7086
+ return { capability, response: data };
7087
+ }
7088
+ async function uploadWithSignedCapability({
7089
+ serverUrl,
7090
+ apiKey,
7091
+ workerUrl,
7092
+ scope,
7093
+ createPath = "/api/uploads",
7094
+ createBody,
7095
+ attestationMetadata,
7096
+ uploadBody,
7097
+ fetchImpl = fetch,
7098
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
7099
+ }) {
7100
+ const { capability, response: session } = await createDirectUploadSession({
7101
+ serverUrl,
7102
+ apiKey,
7103
+ workerUrl,
7104
+ scope,
7105
+ createPath,
7106
+ body: createBody,
7107
+ attestationMetadata,
7108
+ fetchImpl,
7109
+ timeoutMs
7110
+ });
7111
+ const { response: uploadResponse } = await executeResponseRequest(
7112
+ session.upload.url,
7113
+ {
7114
+ method: session.upload.method,
7115
+ headers: session.upload.headers ?? {},
7116
+ body: uploadBody
7117
+ },
7118
+ {
7119
+ toolName: "daemon_direct_upload.put",
7120
+ target: capability.audience,
7121
+ timeoutMs,
7122
+ fetchImpl
7123
+ }
7124
+ );
7125
+ if (!uploadResponse.ok) {
7126
+ throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
7127
+ }
7128
+ return { capability, session, uploadResponse };
7129
+ }
7130
+
7131
+ // src/traceBundleUpload.ts
7132
+ var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
7133
+ var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
7134
+ var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
7135
+ var DEFAULT_MAX_FILES_PER_RUN = 4;
7136
+ var DaemonTraceBundleUploader = class {
7137
+ options;
7138
+ timer = null;
7139
+ constructor(options) {
7140
+ this.options = options;
7141
+ }
7142
+ start() {
7143
+ if (this.timer) return;
7144
+ void this.uploadOnce();
7145
+ this.timer = setInterval(() => {
7146
+ void this.uploadOnce();
7147
+ }, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
7148
+ }
7149
+ stop() {
7150
+ if (!this.timer) return;
7151
+ clearInterval(this.timer);
7152
+ this.timer = null;
7153
+ }
7154
+ async uploadOnce() {
7155
+ const files = await this.findUploadCandidates();
7156
+ let uploaded = 0;
7157
+ for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
7158
+ if (await this.uploadFile(file)) uploaded += 1;
7159
+ }
7160
+ return { attempted: files.length, uploaded };
7161
+ }
7162
+ async findUploadCandidates() {
7163
+ const traceDir = path14.join(this.options.machineDir, "traces");
7164
+ let names;
7165
+ try {
7166
+ names = await readdir3(traceDir);
7167
+ } catch {
7168
+ return [];
7169
+ }
7170
+ const now = Date.now();
7171
+ const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
7172
+ const currentFile = this.options.currentFileProvider?.();
7173
+ const candidates = [];
7174
+ for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
7175
+ const file = path14.join(traceDir, name);
7176
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
7177
+ if (await this.isUploaded(file)) continue;
7178
+ try {
7179
+ const info = await stat3(file);
7180
+ if (!info.isFile() || info.size <= 0) continue;
7181
+ if (now - info.mtimeMs < minAgeMs) continue;
7182
+ candidates.push(file);
7183
+ } catch {
7184
+ }
7185
+ }
7186
+ return candidates;
7187
+ }
7188
+ async uploadFile(file) {
7189
+ const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
7190
+ surface: "daemon",
7191
+ kind: "producer",
7192
+ attrs: {
7193
+ file_present: true,
7194
+ worker_url_present: Boolean(this.options.workerUrl)
7195
+ }
7196
+ });
7197
+ try {
7198
+ const raw = await readFile2(file);
7199
+ if (raw.byteLength === 0) {
7200
+ span?.end("cancelled", { attrs: { outcome: "empty" } });
7201
+ return false;
7202
+ }
7203
+ const gzipped = gzipSync(raw);
7204
+ const bundleSha256 = sha256Hex(gzipped);
7205
+ const bundleId = randomUUID3();
7206
+ await uploadWithSignedCapability({
7207
+ serverUrl: this.options.serverUrl,
7208
+ apiKey: this.options.apiKey,
7209
+ workerUrl: this.options.workerUrl,
7210
+ scope: TRACE_UPLOAD_SCOPE,
7211
+ createPath: "/api/trace-bundles",
7212
+ attestationMetadata: {
7213
+ bundleId,
7214
+ bundleSha256,
7215
+ bundleSizeBytes: gzipped.byteLength
7216
+ },
7217
+ createBody: {
7218
+ bundleSha256,
7219
+ bundleSizeBytes: gzipped.byteLength
7220
+ },
7221
+ uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
7222
+ fetchImpl: this.options.fetchImpl
7223
+ });
7224
+ await this.markUploaded(file, {
7225
+ bundleId,
7226
+ bundleSha256,
7227
+ bundleSizeBytes: gzipped.byteLength
7228
+ });
7229
+ span?.end("ok", {
7230
+ attrs: {
7231
+ bundleId,
7232
+ bundle_size_bytes: gzipped.byteLength
7233
+ }
7234
+ });
7235
+ return true;
7236
+ } catch (err) {
7237
+ span?.end("error", {
7238
+ attrs: {
7239
+ error_class: err instanceof Error ? err.name : "Error",
7240
+ error_message_present: err instanceof Error && Boolean(err.message)
7241
+ }
7242
+ });
7243
+ return false;
7244
+ }
7245
+ }
7246
+ uploadStatePath(file) {
7247
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7248
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
7249
+ }
7250
+ async isUploaded(file) {
7251
+ try {
7252
+ await stat3(this.uploadStatePath(file));
7253
+ return true;
7254
+ } catch {
7255
+ return false;
7256
+ }
7257
+ }
7258
+ async markUploaded(file, metadata) {
7259
+ const stateFile = this.uploadStatePath(file);
7260
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
7261
+ await writeFile2(stateFile, `${JSON.stringify({
7262
+ file: path14.basename(file),
7263
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7264
+ ...metadata
7265
+ }, null, 2)}
7266
+ `, { mode: 384 });
7267
+ }
7268
+ };
7269
+ function sha256Hex(body) {
7270
+ return createHash2("sha256").update(body).digest("hex");
7271
+ }
7272
+ function readPositiveIntegerEnv2(name, fallback) {
7273
+ const value = process.env[name];
7274
+ if (!value) return fallback;
7275
+ const parsed = Number(value);
7276
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
7277
+ }
7278
+
5440
7279
  // src/core.ts
5441
7280
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
5442
7281
  function parseDaemonCliArgs(args) {
@@ -5458,59 +7297,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5458
7297
  }
5459
7298
  }
5460
7299
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5461
- const dirname = path13.dirname(fileURLToPath(moduleUrl));
5462
- const jsPath = path13.resolve(dirname, "chat-bridge.js");
7300
+ const dirname = path15.dirname(fileURLToPath(moduleUrl));
7301
+ const jsPath = path15.resolve(dirname, "chat-bridge.js");
5463
7302
  try {
5464
7303
  accessSync(jsPath);
5465
7304
  return jsPath;
5466
7305
  } catch {
5467
- return path13.resolve(dirname, "chat-bridge.ts");
7306
+ return path15.resolve(dirname, "chat-bridge.ts");
5468
7307
  }
5469
7308
  }
5470
7309
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5471
- const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5472
- const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
7310
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7311
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
5473
7312
  try {
5474
7313
  accessSync(bundledDistPath);
5475
7314
  return bundledDistPath;
5476
7315
  } catch {
5477
- const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7316
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5478
7317
  accessSync(workspaceDistPath);
5479
7318
  return workspaceDistPath;
5480
7319
  }
5481
7320
  }
5482
- function detectRuntimes() {
7321
+ function detectRuntimes(tracer = noopTracer) {
5483
7322
  const ids = [];
5484
7323
  const versions = {};
7324
+ const span = tracer.startSpan("daemon.runtime.detect", {
7325
+ surface: "daemon",
7326
+ kind: "internal",
7327
+ attrs: {
7328
+ known_runtime_count: RUNTIMES.length
7329
+ }
7330
+ });
5485
7331
  for (const runtime of RUNTIMES) {
5486
7332
  const driver = getDriver(runtime.id);
7333
+ let probeErrorPresent = false;
5487
7334
  try {
5488
7335
  if (driver.probe) {
5489
7336
  const probe = driver.probe();
5490
7337
  if (!probe.available) {
5491
7338
  if (probe.version) versions[runtime.id] = probe.version;
7339
+ span.addEvent("daemon.runtime.detect.checked", {
7340
+ runtime: runtime.id,
7341
+ outcome: "unavailable",
7342
+ version_present: Boolean(probe.version),
7343
+ binary_path_present: false
7344
+ });
5492
7345
  continue;
5493
7346
  }
5494
7347
  ids.push(runtime.id);
5495
7348
  if (probe.version) versions[runtime.id] = probe.version;
7349
+ span.addEvent("daemon.runtime.detect.checked", {
7350
+ runtime: runtime.id,
7351
+ outcome: "available",
7352
+ version_present: Boolean(probe.version),
7353
+ binary_path_present: false
7354
+ });
5496
7355
  continue;
5497
7356
  }
5498
7357
  } catch {
7358
+ probeErrorPresent = true;
5499
7359
  }
5500
7360
  const detectionBinaries = [runtime.binary];
7361
+ let detectedByPath = false;
5501
7362
  for (const binary of detectionBinaries) {
5502
7363
  const resolved = resolveCommandOnPath(binary);
5503
7364
  if (!resolved) continue;
5504
7365
  ids.push(runtime.id);
7366
+ detectedByPath = true;
5505
7367
  const version = readCommandVersion(binary);
5506
7368
  if (version) {
5507
7369
  versions[runtime.id] = version;
5508
7370
  }
7371
+ span.addEvent("daemon.runtime.detect.checked", {
7372
+ runtime: runtime.id,
7373
+ outcome: "available",
7374
+ version_present: Boolean(version),
7375
+ binary_path_present: true,
7376
+ probe_error_present: probeErrorPresent
7377
+ });
5509
7378
  break;
5510
7379
  }
7380
+ if (!detectedByPath) {
7381
+ span.addEvent("daemon.runtime.detect.checked", {
7382
+ runtime: runtime.id,
7383
+ outcome: "unavailable",
7384
+ version_present: false,
7385
+ binary_path_present: false,
7386
+ probe_error_present: probeErrorPresent
7387
+ });
7388
+ }
5511
7389
  }
7390
+ span.end("ok", {
7391
+ attrs: {
7392
+ detected_runtime_count: ids.length
7393
+ }
7394
+ });
5512
7395
  return { ids, versions };
5513
7396
  }
7397
+ function readPositiveIntegerEnv3(name, fallback) {
7398
+ const raw = process.env[name];
7399
+ if (!raw) return fallback;
7400
+ const parsed = Number(raw);
7401
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
7402
+ return Math.floor(parsed);
7403
+ }
5514
7404
  function formatChannelTarget(msg) {
5515
7405
  return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
5516
7406
  }
@@ -5534,6 +7424,8 @@ function summarizeIncomingMessage(msg) {
5534
7424
  return `(agent=${msg.agentId}, path=${msg.path})`;
5535
7425
  case "agent:skills:list":
5536
7426
  return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
7427
+ case "agent:activity_probe":
7428
+ return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
5537
7429
  case "machine:workspace:delete":
5538
7430
  return `(directory=${msg.directoryName})`;
5539
7431
  case "machine:runtime_models:detect":
@@ -5558,14 +7450,18 @@ var DaemonCore = class {
5558
7450
  connection;
5559
7451
  reminderCache;
5560
7452
  tracer;
7453
+ injectedTracer;
5561
7454
  machineLock = null;
7455
+ localTraceSink = null;
7456
+ traceBundleUploader = null;
5562
7457
  constructor(options) {
5563
7458
  this.options = options;
5564
7459
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
5565
7460
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
5566
7461
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
5567
- this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
7462
+ this.injectedTracer = Boolean(options.tracer);
5568
7463
  this.tracer = options.tracer ?? noopTracer;
7464
+ this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
5569
7465
  this.reminderCache = new ReminderCache({
5570
7466
  clock: options.reminderClock,
5571
7467
  onFire: (job) => this.onReminderFire(job)
@@ -5575,7 +7471,8 @@ var DaemonCore = class {
5575
7471
  dataDir: options.dataDir,
5576
7472
  serverUrl: options.serverUrl,
5577
7473
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
5578
- slockCliPath: this.slockCliPath
7474
+ slockCliPath: this.slockCliPath,
7475
+ tracer: this.tracer
5579
7476
  };
5580
7477
  this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
5581
7478
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -5585,15 +7482,48 @@ var DaemonCore = class {
5585
7482
  ...options.connectionOptions,
5586
7483
  onMessage: (msg) => this.handleMessage(msg),
5587
7484
  onConnect: () => this.handleConnect(),
5588
- onDisconnect: () => this.handleDisconnect()
7485
+ onDisconnect: () => this.handleDisconnect(),
7486
+ onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
5589
7487
  });
5590
7488
  this.connection = connection;
5591
7489
  }
5592
7490
  resolveMachineStateRoot() {
5593
7491
  if (this.options.machineStateDir) return this.options.machineStateDir;
5594
- if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
7492
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
5595
7493
  return DEFAULT_MACHINE_STATE_ROOT;
5596
7494
  }
7495
+ shouldEnableLocalTrace() {
7496
+ if (this.injectedTracer) return false;
7497
+ if (!this.options.localTrace) return false;
7498
+ return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
7499
+ }
7500
+ installLocalTraceSink(machineDir) {
7501
+ if (!this.shouldEnableLocalTrace()) return;
7502
+ this.localTraceSink = new LocalRotatingTraceSink({
7503
+ machineDir,
7504
+ maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
7505
+ maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
7506
+ });
7507
+ this.tracer = new BasicTracer({
7508
+ sink: this.localTraceSink
7509
+ });
7510
+ this.agentManager.setTracer(this.tracer);
7511
+ }
7512
+ installTraceBundleUploader(machineDir) {
7513
+ if (!this.shouldEnableLocalTrace()) return;
7514
+ if (this.traceBundleUploader) return;
7515
+ const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
7516
+ if (!workerUrl) return;
7517
+ this.traceBundleUploader = new DaemonTraceBundleUploader({
7518
+ machineDir,
7519
+ serverUrl: this.options.serverUrl,
7520
+ apiKey: this.options.apiKey,
7521
+ workerUrl,
7522
+ tracer: this.tracer,
7523
+ currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
7524
+ });
7525
+ this.traceBundleUploader.start();
7526
+ }
5597
7527
  start() {
5598
7528
  logger.info("[Slock Daemon] Starting...");
5599
7529
  if (!this.machineLock) {
@@ -5603,10 +7533,24 @@ var DaemonCore = class {
5603
7533
  rootDir: this.resolveMachineStateRoot()
5604
7534
  });
5605
7535
  logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
7536
+ this.installLocalTraceSink(this.machineLock.machineDir);
7537
+ this.installTraceBundleUploader(this.machineLock.machineDir);
7538
+ const span = this.tracer.startSpan("daemon.lifecycle.start", {
7539
+ surface: "daemon",
7540
+ kind: "internal",
7541
+ attrs: {
7542
+ machine_dir_present: true,
7543
+ local_trace_enabled: this.shouldEnableLocalTrace()
7544
+ }
7545
+ });
7546
+ span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
7547
+ span.end("ok");
5606
7548
  }
5607
7549
  try {
5608
7550
  this.connection.connect();
5609
7551
  } catch (err) {
7552
+ this.traceBundleUploader?.stop();
7553
+ this.traceBundleUploader = null;
5610
7554
  this.machineLock.release();
5611
7555
  this.machineLock = null;
5612
7556
  throw err;
@@ -5614,13 +7558,24 @@ var DaemonCore = class {
5614
7558
  }
5615
7559
  async stop() {
5616
7560
  logger.info("[Slock Daemon] Shutting down...");
7561
+ const span = this.tracer.startSpan("daemon.lifecycle.stop", {
7562
+ surface: "daemon",
7563
+ kind: "internal",
7564
+ attrs: { machine_lock_present: Boolean(this.machineLock) }
7565
+ });
5617
7566
  this.reminderCache.clear();
7567
+ this.traceBundleUploader?.stop();
7568
+ this.traceBundleUploader = null;
5618
7569
  try {
5619
7570
  await this.agentManager.stopAll();
7571
+ span.addEvent("daemon.agents.stopped");
5620
7572
  } finally {
5621
7573
  this.connection.disconnect();
7574
+ span.addEvent("daemon.connection.disconnect_requested");
5622
7575
  this.machineLock?.release();
7576
+ if (this.machineLock) span.addEvent("daemon.machine_lock.released");
5623
7577
  this.machineLock = null;
7578
+ span.end("ok");
5624
7579
  }
5625
7580
  }
5626
7581
  get connected() {
@@ -5629,6 +7584,14 @@ var DaemonCore = class {
5629
7584
  getRunningAgentIds() {
5630
7585
  return this.agentManager.getRunningAgentIds();
5631
7586
  }
7587
+ recordDaemonTrace(name, attrs, status = "ok") {
7588
+ const span = this.tracer.startSpan(name, {
7589
+ surface: "daemon",
7590
+ kind: "internal",
7591
+ attrs
7592
+ });
7593
+ span.end(status);
7594
+ }
5632
7595
  handleMessage(msg) {
5633
7596
  const summary = summarizeIncomingMessage(msg);
5634
7597
  logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
@@ -5658,50 +7621,88 @@ var DaemonCore = class {
5658
7621
  kind: "consumer",
5659
7622
  attrs: {
5660
7623
  agentId: msg.agentId,
7624
+ deliveryId: msg.deliveryId,
7625
+ delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
7626
+ messageId: msg.message.message_id,
7627
+ message_id_present: Boolean(msg.message.message_id),
5661
7628
  seq: msg.seq
5662
7629
  }
5663
7630
  });
5664
7631
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
5665
7632
  try {
5666
- span.addEvent("daemon.receive", { seq: msg.seq });
5667
- this.agentManager.deliverMessage(msg.agentId, msg.message);
5668
- span.addEvent("daemon.deliver_to_agent_manager");
7633
+ span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
7634
+ const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
7635
+ span.addEvent("daemon.deliver_to_agent_manager", { accepted });
7636
+ if (!accepted) {
7637
+ span.end("ok", { attrs: { outcome: "not-accepted" } });
7638
+ break;
7639
+ }
5669
7640
  const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
5670
7641
  span.addEvent("daemon.ack.sent", { seq: ackSeq });
5671
7642
  this.connection.send({
5672
7643
  type: "agent:deliver:ack",
5673
7644
  agentId: msg.agentId,
5674
7645
  seq: ackSeq,
5675
- traceparent: formatTraceparent(span.context)
7646
+ traceparent: formatTraceparent(span.context),
7647
+ deliveryId: msg.deliveryId
5676
7648
  });
5677
- span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
7649
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
5678
7650
  } catch (err) {
5679
- span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
7651
+ span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
5680
7652
  throw err;
5681
7653
  }
5682
7654
  break;
5683
7655
  }
5684
- case "agent:runtime_profile:migration":
7656
+ case "agent:runtime_profile:migration": {
7657
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7658
+ parent: parseTraceparent(msg.traceparent),
7659
+ surface: "daemon",
7660
+ kind: "consumer",
7661
+ attrs: {
7662
+ agentId: msg.agentId,
7663
+ control_kind: "migration",
7664
+ key_present: Boolean(msg.migrationKey),
7665
+ launchId: msg.launchId || void 0
7666
+ }
7667
+ });
5685
7668
  logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
5686
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
7669
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
7670
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
5687
7671
  break;
5688
- case "agent:runtime_profile:daemon_release_notice":
7672
+ }
7673
+ case "agent:runtime_profile:daemon_release_notice": {
7674
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7675
+ parent: parseTraceparent(msg.traceparent),
7676
+ surface: "daemon",
7677
+ kind: "consumer",
7678
+ attrs: {
7679
+ agentId: msg.agentId,
7680
+ control_kind: "daemon_release_notice",
7681
+ key_present: Boolean(msg.noticeKey),
7682
+ launchId: msg.launchId || void 0
7683
+ }
7684
+ });
5689
7685
  logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
5690
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
7686
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
7687
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
5691
7688
  break;
7689
+ }
5692
7690
  case "agent:workspace:list":
5693
7691
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
5694
7692
  this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
5695
7693
  });
5696
7694
  break;
5697
7695
  case "agent:workspace:read":
5698
- this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
7696
+ this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
5699
7697
  this.connection.send({
5700
7698
  type: "agent:workspace:file_content",
5701
7699
  agentId: msg.agentId,
5702
7700
  requestId: msg.requestId,
5703
7701
  content,
5704
- binary
7702
+ binary,
7703
+ size,
7704
+ mimeType,
7705
+ encoding
5705
7706
  });
5706
7707
  }).catch(() => {
5707
7708
  this.connection.send({
@@ -5709,7 +7710,8 @@ var DaemonCore = class {
5709
7710
  agentId: msg.agentId,
5710
7711
  requestId: msg.requestId,
5711
7712
  content: null,
5712
- binary: false
7713
+ binary: false,
7714
+ size: 0
5713
7715
  });
5714
7716
  });
5715
7717
  break;
@@ -5721,6 +7723,9 @@ var DaemonCore = class {
5721
7723
  this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
5722
7724
  });
5723
7725
  break;
7726
+ case "agent:activity_probe":
7727
+ this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
7728
+ break;
5724
7729
  case "machine:workspace:scan":
5725
7730
  logger.info("[Daemon] Scanning all workspace directories");
5726
7731
  this.agentManager.scanAllWorkspaces().then((directories) => {
@@ -5738,7 +7743,12 @@ var DaemonCore = class {
5738
7743
  const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
5739
7744
  Promise.resolve(detect).then((result) => {
5740
7745
  if (result) {
5741
- this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models: result.models, default: result.default });
7746
+ const verified = driver.model.detectedModelsVerifiedAs;
7747
+ const models = result.models.map((model) => ({
7748
+ ...model,
7749
+ verified: model.verified ?? verified
7750
+ }));
7751
+ this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
5742
7752
  } else {
5743
7753
  this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
5744
7754
  }
@@ -5777,35 +7787,59 @@ var DaemonCore = class {
5777
7787
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
5778
7788
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
5779
7789
  logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
7790
+ const runningAgentIds = this.agentManager.getRunningAgentIds();
7791
+ const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
7792
+ const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
5780
7793
  this.connection.send({
5781
7794
  type: "ready",
5782
7795
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
5783
7796
  runtimes,
5784
- runningAgents: this.agentManager.getRunningAgentIds(),
5785
- hostname: this.options.hostname ?? os6.hostname(),
5786
- os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
7797
+ runningAgents: runningAgentIds,
7798
+ hostname: this.options.hostname ?? os7.hostname(),
7799
+ os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
5787
7800
  daemonVersion: this.daemonVersion
5788
7801
  });
5789
- for (const agentId of this.agentManager.getRunningAgentIds()) {
7802
+ this.recordDaemonTrace("daemon.ready.sent", {
7803
+ runtimes_count: runtimes.length,
7804
+ running_agents_count: runningAgentIds.length,
7805
+ idle_agents_count: idleAgentSessions.length,
7806
+ runtime_profile_reports_count: runtimeProfileReports.length,
7807
+ daemon_version_present: Boolean(this.daemonVersion)
7808
+ });
7809
+ for (const agentId of runningAgentIds) {
5790
7810
  const sessionId = this.agentManager.getAgentSessionId(agentId);
5791
7811
  const launchId = this.agentManager.getAgentLaunchId(agentId);
5792
7812
  if (sessionId) {
5793
7813
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
5794
7814
  }
5795
7815
  }
5796
- for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
7816
+ for (const { agentId, sessionId, launchId } of idleAgentSessions) {
5797
7817
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
5798
7818
  }
5799
- for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
7819
+ for (const report of runtimeProfileReports) {
7820
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
7821
+ surface: "daemon",
7822
+ kind: "producer",
7823
+ attrs: {
7824
+ agentId: report.agentId,
7825
+ launchId: report.launchId || void 0,
7826
+ runtime: report.facts.runtime,
7827
+ model_present: Boolean(report.facts.model),
7828
+ session_ref_present: Boolean(report.facts.sessionRef),
7829
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
7830
+ }
7831
+ });
5800
7832
  this.connection.send({
5801
7833
  type: "agent:runtime_profile",
5802
7834
  agentId: report.agentId,
5803
7835
  facts: report.facts,
5804
- launchId: report.launchId || void 0
7836
+ launchId: report.launchId || void 0,
7837
+ traceparent: formatTraceparent(span.context)
5805
7838
  });
7839
+ span.end("ok");
5806
7840
  }
5807
- const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
5808
- for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
7841
+ const agentsForSnapshot = new Set(runningAgentIds);
7842
+ for (const { agentId } of idleAgentSessions) {
5809
7843
  agentsForSnapshot.add(agentId);
5810
7844
  }
5811
7845
  for (const agentId of agentsForSnapshot) {
@@ -5815,6 +7849,10 @@ var DaemonCore = class {
5815
7849
  }
5816
7850
  handleDisconnect() {
5817
7851
  logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
7852
+ this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
7853
+ running_agents_count: this.agentManager.getRunningAgentIds().length,
7854
+ idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
7855
+ }, "cancelled");
5818
7856
  this.options.lifecycleHooks?.onDisconnect?.();
5819
7857
  }
5820
7858
  };