@slock-ai/daemon 0.43.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,14 @@
1
1
  import {
2
+ DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
2
3
  buildWebSocketOptions,
4
+ executeJsonRequest,
5
+ executeResponseRequest,
3
6
  logger
4
- } from "./chunk-JG7ONJZ6.js";
7
+ } from "./chunk-Z3PCMYZO.js";
5
8
 
6
9
  // src/core.ts
7
- import path13 from "path";
8
- import os6 from "os";
10
+ import path15 from "path";
11
+ import os7 from "os";
9
12
  import { createRequire } from "module";
10
13
  import { accessSync } from "fs";
11
14
  import { fileURLToPath } from "url";
@@ -94,6 +97,103 @@ var NoopActiveSpan = class {
94
97
  }
95
98
  };
96
99
  var noopTracer = new NoopTracer();
100
+ var BasicTracer = class {
101
+ sink;
102
+ clock;
103
+ traceIdGenerator;
104
+ spanIdGenerator;
105
+ constructor({
106
+ sink,
107
+ clock = () => Date.now(),
108
+ traceIdGenerator = generateTraceId,
109
+ spanIdGenerator = generateSpanId
110
+ }) {
111
+ this.sink = sink;
112
+ this.clock = clock;
113
+ this.traceIdGenerator = traceIdGenerator;
114
+ this.spanIdGenerator = spanIdGenerator;
115
+ }
116
+ startSpan(name, options) {
117
+ const startTimeMs = options.startTimeMs ?? this.clock();
118
+ const context = createTraceContext({
119
+ parent: options.parent ?? null,
120
+ traceIdGenerator: this.traceIdGenerator,
121
+ spanIdGenerator: this.spanIdGenerator
122
+ });
123
+ return new RecordingActiveSpan({
124
+ context,
125
+ name,
126
+ surface: options.surface,
127
+ kind: options.kind ?? "internal",
128
+ attrs: options.attrs,
129
+ startTimeMs,
130
+ clock: this.clock,
131
+ sink: this.sink
132
+ });
133
+ }
134
+ };
135
+ var RecordingActiveSpan = class {
136
+ context;
137
+ name;
138
+ surface;
139
+ kind;
140
+ startTimeMs;
141
+ clock;
142
+ sink;
143
+ events = [];
144
+ attrs;
145
+ ended = false;
146
+ constructor({
147
+ context,
148
+ name,
149
+ surface,
150
+ kind,
151
+ attrs,
152
+ startTimeMs,
153
+ clock,
154
+ sink
155
+ }) {
156
+ this.context = context;
157
+ this.name = name;
158
+ this.surface = surface;
159
+ this.kind = kind;
160
+ this.attrs = attrs;
161
+ this.startTimeMs = startTimeMs;
162
+ this.clock = clock;
163
+ this.sink = sink;
164
+ }
165
+ addEvent(name, attrs) {
166
+ if (this.ended) return;
167
+ this.events.push({
168
+ name,
169
+ timeMs: this.clock(),
170
+ ...attrs ? { attrs } : {}
171
+ });
172
+ }
173
+ end(status = "ok", options = {}) {
174
+ if (this.ended) return;
175
+ this.ended = true;
176
+ const endTimeMs = this.clock();
177
+ const attrs = mergeAttrs(this.attrs, options.attrs);
178
+ this.sink.record({
179
+ context: this.context,
180
+ name: this.name,
181
+ surface: this.surface,
182
+ kind: this.kind,
183
+ status,
184
+ startTimeMs: this.startTimeMs,
185
+ endTimeMs,
186
+ durationMs: Math.max(0, endTimeMs - this.startTimeMs),
187
+ ...attrs ? { attrs } : {},
188
+ events: [...this.events]
189
+ });
190
+ }
191
+ };
192
+ function mergeAttrs(base, extra) {
193
+ if (!base) return extra;
194
+ if (!extra) return base;
195
+ return { ...base, ...extra };
196
+ }
97
197
  function generateTraceId() {
98
198
  return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
99
199
  }
@@ -473,6 +573,9 @@ function summarizeToolInput(toolName, input) {
473
573
  }
474
574
  }
475
575
 
576
+ // ../shared/src/attachmentPreview.ts
577
+ var CSV_PREVIEW_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
578
+
476
579
  // ../shared/src/testing/failpoints.ts
477
580
  var NoopFailpointRegistry = class {
478
581
  get enabled() {
@@ -548,54 +651,6 @@ var RUNTIMES = [
548
651
  { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
549
652
  { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
550
653
  ];
551
- var RUNTIME_MODELS = {
552
- claude: [
553
- { id: "sonnet", label: "Sonnet" },
554
- { id: "opus", label: "Opus" },
555
- { id: "haiku", label: "Haiku" }
556
- ],
557
- codex: [
558
- { id: "gpt-5.5", label: "GPT-5.5" },
559
- { id: "gpt-5.4", label: "GPT-5.4" },
560
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
561
- { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
562
- { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
563
- { id: "gpt-5.2", label: "GPT-5.2" },
564
- { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
565
- { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
566
- { id: "gpt-5-codex", label: "GPT-5 Codex" },
567
- { id: "gpt-5", label: "GPT-5" }
568
- ],
569
- copilot: [
570
- { id: "gpt-5.4", label: "GPT-5.4" },
571
- { id: "gpt-5.2", label: "GPT-5.2" },
572
- { id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
573
- { id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
574
- ],
575
- cursor: [
576
- { id: "composer-2-fast", label: "Composer 2 Fast" },
577
- { id: "composer-2", label: "Composer 2" },
578
- { id: "auto", label: "Auto" }
579
- ],
580
- gemini: [
581
- { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
582
- { id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
583
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
584
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
585
- ],
586
- opencode: [
587
- { id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
588
- { id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
589
- { id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
590
- { id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
591
- { id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
592
- ],
593
- // Kimi CLI resolves model keys from each user's local config, so the safest
594
- // built-in option is to defer to whatever default model the CLI already uses.
595
- kimi: [
596
- { id: "default", label: "Configured Default" }
597
- ]
598
- };
599
654
  var PLAN_CONFIG = {
600
655
  free: {
601
656
  displayName: "Hobby",
@@ -631,14 +686,15 @@ var DISPLAY_PLAN_CONFIG = {
631
686
  };
632
687
 
633
688
  // src/agentProcessManager.ts
634
- import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
689
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
635
690
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
636
691
  import path11 from "path";
637
- import os4 from "os";
692
+ import os5 from "os";
638
693
 
639
694
  // src/drivers/claude.ts
640
695
  import { spawn } from "child_process";
641
- import { writeFileSync as writeFileSync2 } from "fs";
696
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
697
+ import os from "os";
642
698
  import path3 from "path";
643
699
 
644
700
  // src/drivers/cliTransport.ts
@@ -735,8 +791,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
735
791
  16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
736
792
  17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
737
793
  18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
738
- 19. **\`slock reminder list\`** \u2014 List your reminders.
739
- 20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
794
+ 19. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
795
+ 20. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
796
+ 21. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
797
+ 22. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
798
+ 23. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
740
799
 
741
800
  The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
742
801
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -759,22 +818,23 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
759
818
  9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
760
819
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
761
820
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
762
- 12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
821
+ 12. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
763
822
  13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
764
823
  14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
765
824
  15. **${listRemindersCmd}** \u2014 List your reminders.
766
825
  16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
767
826
  const reminderSection = `### Reminders
768
827
 
769
- Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
770
- Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
828
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
829
+ When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
830
+ Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
771
831
  Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
772
832
  const sendingMessagesSection = isCli ? `### Sending messages
773
833
 
774
834
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
775
- - **Reply to a DM**: \`slock message send --target "dm:@peer-name" <<'EOF'\` followed by the message body and \`EOF\`
835
+ - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'EOF'\` followed by the message body and \`EOF\`
776
836
  - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
777
- - **Start a NEW DM**: \`slock message send --target "dm:@person-name" <<'EOF'\` followed by the message body and \`EOF\`
837
+ - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'EOF'\` followed by the message body and \`EOF\`
778
838
 
779
839
  Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
780
840
  \`\`\`bash
@@ -814,10 +874,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
814
874
  const discoverySection = isCli ? `### Discovering people and channels
815
875
 
816
876
  Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
817
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
877
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.
878
+ Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
818
879
 
819
880
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
820
- Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
881
+ Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.
882
+ Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
821
883
  const channelAwarenessSection = isCli ? `### Channel awareness
822
884
 
823
885
  Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
@@ -831,7 +893,7 @@ Each channel has a **name** and optionally a **description** that define its pur
831
893
  - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
832
894
  const readingHistorySection = isCli ? `### Reading history
833
895
 
834
- \`slock message read --channel "#channel-name"\` or \`slock message read --channel "dm:@peer-name"\` or \`slock message read --channel "#channel:shortid"\`
896
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
835
897
 
836
898
  To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
837
899
 
@@ -923,7 +985,7 @@ ${readCmd} shows messages in their current state. If a message was later convert
923
985
  - Reuse existing tasks and threads instead of creating duplicates.
924
986
  - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
925
987
  const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
926
- let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
988
+ let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.
927
989
 
928
990
  ## Who you are
929
991
 
@@ -1301,6 +1363,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
1301
1363
  var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
1302
1364
  var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
1303
1365
  var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
1366
+ var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
1304
1367
  var CLAUDE_DISALLOWED_TOOLS = [
1305
1368
  "EnterPlanMode",
1306
1369
  "ExitPlanMode",
@@ -1326,10 +1389,101 @@ function probeClaude(deps = {}) {
1326
1389
  version: readCommandVersion(command, [], deps) ?? void 0
1327
1390
  };
1328
1391
  }
1392
+ function isRecord(value) {
1393
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1394
+ }
1395
+ function expandClaudeMcpConfigVariables(raw, vars) {
1396
+ let expanded = raw;
1397
+ for (const [name, value] of Object.entries(vars)) {
1398
+ expanded = expanded.replaceAll(`\${${name}}`, value);
1399
+ }
1400
+ return expanded;
1401
+ }
1402
+ function readClaudeMcpServers(configPath, vars = {}) {
1403
+ try {
1404
+ const parsed = JSON.parse(
1405
+ expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
1406
+ );
1407
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
1408
+ return parsed.mcpServers;
1409
+ } catch (err) {
1410
+ logger.warn(
1411
+ `[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
1412
+ );
1413
+ return null;
1414
+ }
1415
+ }
1416
+ function resolveClaudeConfigDir(ctx, home) {
1417
+ const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
1418
+ return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
1419
+ }
1420
+ function collectClaudeMcpConfigFiles(ctx, home) {
1421
+ const files = [];
1422
+ const pushIfFile = (candidate) => {
1423
+ try {
1424
+ if (existsSync2(candidate) && statSync(candidate).isFile()) {
1425
+ files.push({ path: candidate });
1426
+ }
1427
+ } catch {
1428
+ }
1429
+ };
1430
+ pushIfFile(path3.join(home, ".claude.json"));
1431
+ pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
1432
+ const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
1433
+ try {
1434
+ for (const entry of readdirSync(pluginRoot)) {
1435
+ const pluginPath = path3.join(pluginRoot, entry);
1436
+ const configPath = path3.join(pluginPath, ".mcp.json");
1437
+ try {
1438
+ if (existsSync2(configPath) && statSync(configPath).isFile()) {
1439
+ files.push({
1440
+ path: configPath,
1441
+ vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
1442
+ });
1443
+ }
1444
+ } catch {
1445
+ }
1446
+ }
1447
+ } catch {
1448
+ }
1449
+ return files;
1450
+ }
1451
+ function buildClaudeUserMcpServers(ctx, home) {
1452
+ const servers = /* @__PURE__ */ Object.create(null);
1453
+ for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
1454
+ const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
1455
+ if (!mcpServers) continue;
1456
+ for (const [name, server] of Object.entries(mcpServers)) {
1457
+ if (!isRecord(server)) {
1458
+ logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
1459
+ continue;
1460
+ }
1461
+ servers[name] = server;
1462
+ }
1463
+ }
1464
+ return servers;
1465
+ }
1329
1466
  var ClaudeDriver = class {
1330
1467
  id = "claude";
1468
+ lifecycle = {
1469
+ kind: "persistent",
1470
+ stdin: "gated",
1471
+ inFlightWake: "queue"
1472
+ };
1473
+ communication = {
1474
+ chat: "slock_cli",
1475
+ runtimeControl: "mcp_runtime_actions"
1476
+ };
1477
+ session = {
1478
+ recovery: "resume_or_fresh"
1479
+ };
1480
+ model = {
1481
+ detectedModelsVerifiedAs: "launchable",
1482
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
1483
+ };
1331
1484
  supportsStdinNotification = true;
1332
1485
  mcpToolPrefix = "mcp__chat__";
1486
+ usesSlockCliForCommunication = true;
1333
1487
  // Claude Code supports same-turn steering, but raw stdin injection at an
1334
1488
  // arbitrary busy instant can collide with active signed thinking blocks. The
1335
1489
  // daemon therefore gates busy delivery on Claude stream-json boundaries.
@@ -1364,36 +1518,46 @@ var ClaudeDriver = class {
1364
1518
  }
1365
1519
  return args;
1366
1520
  }
1367
- buildRuntimeActionsMcpConfig(ctx) {
1521
+ buildRuntimeActionsMcpServer(ctx) {
1368
1522
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
1369
1523
  const command = isTsSource ? "npx" : "node";
1370
1524
  const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
1525
+ return {
1526
+ command,
1527
+ args: [
1528
+ ...bridgeArgs,
1529
+ "--agent-id",
1530
+ ctx.agentId,
1531
+ "--server-url",
1532
+ ctx.config.serverUrl,
1533
+ "--auth-token",
1534
+ ctx.config.authToken || ctx.daemonApiKey,
1535
+ "--runtime",
1536
+ this.id,
1537
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
1538
+ "--runtime-actions-only"
1539
+ ]
1540
+ };
1541
+ }
1542
+ buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
1543
+ const userMcpServers = buildClaudeUserMcpServers(ctx, home);
1544
+ if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
1545
+ logger.warn(
1546
+ `[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
1547
+ );
1548
+ }
1371
1549
  return JSON.stringify({
1372
1550
  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
- }
1551
+ ...userMcpServers,
1552
+ [SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
1389
1553
  }
1390
1554
  });
1391
1555
  }
1392
- writeClaudeLaunchFiles(ctx, slockDir) {
1556
+ writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
1393
1557
  const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
1394
1558
  const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
1395
1559
  writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
1396
- writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
1560
+ writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
1397
1561
  return { systemPromptPath, mcpConfigPath };
1398
1562
  }
1399
1563
  spawn(ctx) {
@@ -1541,8 +1705,8 @@ var ClaudeDriver = class {
1541
1705
 
1542
1706
  // src/drivers/codex.ts
1543
1707
  import { spawn as spawn2, execSync } from "child_process";
1544
- import { existsSync as existsSync2, readFileSync } from "fs";
1545
- import os from "os";
1708
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1709
+ import os2 from "os";
1546
1710
  import path4 from "path";
1547
1711
  function getCodexNotificationErrorMessage(params) {
1548
1712
  const topLevelMessage = params?.message;
@@ -1556,7 +1720,7 @@ function getCodexNotificationErrorMessage(params) {
1556
1720
  return null;
1557
1721
  }
1558
1722
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1559
- const existsSyncFn = deps.existsSyncFn ?? existsSync2;
1723
+ const existsSyncFn = deps.existsSyncFn ?? existsSync3;
1560
1724
  const execSyncFn = deps.execSyncFn ?? execSync;
1561
1725
  const gitDir = path4.join(workingDirectory, ".git");
1562
1726
  if (existsSyncFn(gitDir)) return;
@@ -1603,14 +1767,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1603
1767
  try {
1604
1768
  const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1605
1769
  const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
1606
- if (existsSync2(candidate)) codexEntry = candidate;
1770
+ if (existsSync3(candidate)) codexEntry = candidate;
1607
1771
  } catch {
1608
1772
  }
1609
1773
  if (!codexEntry) {
1610
1774
  try {
1611
1775
  const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
1612
1776
  const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
1613
- if (existsSync2(candidate)) codexEntry = candidate;
1777
+ if (existsSync3(candidate)) codexEntry = candidate;
1614
1778
  } catch {
1615
1779
  }
1616
1780
  }
@@ -1631,8 +1795,25 @@ function joinReasoningText(item) {
1631
1795
  }
1632
1796
  var CodexDriver = class {
1633
1797
  id = "codex";
1798
+ lifecycle = {
1799
+ kind: "persistent",
1800
+ stdin: "direct",
1801
+ inFlightWake: "steer"
1802
+ };
1803
+ communication = {
1804
+ chat: "slock_cli",
1805
+ runtimeControl: "mcp_runtime_actions"
1806
+ };
1807
+ session = {
1808
+ recovery: "resume_or_fresh"
1809
+ };
1810
+ model = {
1811
+ detectedModelsVerifiedAs: "launchable",
1812
+ toLaunchSpec: (modelId) => ({ params: { model: modelId } })
1813
+ };
1634
1814
  supportsStdinNotification = true;
1635
1815
  mcpToolPrefix = "mcp_chat_";
1816
+ usesSlockCliForCommunication = true;
1636
1817
  busyDeliveryMode = "direct";
1637
1818
  supportsNativeStandingPrompt = true;
1638
1819
  probe() {
@@ -2013,12 +2194,12 @@ var CodexDriver = class {
2013
2194
  return detectCodexModels();
2014
2195
  }
2015
2196
  };
2016
- function detectCodexModels(home = os.homedir()) {
2197
+ function detectCodexModels(home = os2.homedir()) {
2017
2198
  const cachePath = path4.join(home, ".codex", "models_cache.json");
2018
2199
  const configPath = path4.join(home, ".codex", "config.toml");
2019
2200
  let models = [];
2020
2201
  try {
2021
- const raw = readFileSync(cachePath, "utf8");
2202
+ const raw = readFileSync2(cachePath, "utf8");
2022
2203
  const parsed = JSON.parse(raw);
2023
2204
  const entries = Array.isArray(parsed?.models) ? parsed.models : [];
2024
2205
  for (const entry of entries) {
@@ -2027,7 +2208,7 @@ function detectCodexModels(home = os.homedir()) {
2027
2208
  if (entry?.visibility && entry.visibility !== "public") continue;
2028
2209
  if (entry?.supported_in_api === false) continue;
2029
2210
  const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
2030
- models.push({ id: slug, label });
2211
+ models.push({ id: slug, label, verified: "launchable" });
2031
2212
  }
2032
2213
  } catch {
2033
2214
  return null;
@@ -2035,7 +2216,7 @@ function detectCodexModels(home = os.homedir()) {
2035
2216
  if (models.length === 0) return null;
2036
2217
  let defaultModel;
2037
2218
  try {
2038
- const raw = readFileSync(configPath, "utf8");
2219
+ const raw = readFileSync2(configPath, "utf8");
2039
2220
  const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
2040
2221
  if (match) defaultModel = match[1];
2041
2222
  } catch {
@@ -2049,6 +2230,23 @@ import path5 from "path";
2049
2230
  import { writeFileSync as writeFileSync3 } from "fs";
2050
2231
  var CopilotDriver = class {
2051
2232
  id = "copilot";
2233
+ lifecycle = {
2234
+ kind: "per_turn",
2235
+ start: "immediate",
2236
+ exit: "natural",
2237
+ inFlightWake: "spawn_new"
2238
+ };
2239
+ communication = {
2240
+ chat: "mcp_chat_bridge",
2241
+ runtimeControl: "mcp_runtime_actions"
2242
+ };
2243
+ session = {
2244
+ recovery: "resume_or_fresh"
2245
+ };
2246
+ model = {
2247
+ detectedModelsVerifiedAs: "launchable",
2248
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2249
+ };
2052
2250
  supportsStdinNotification = false;
2053
2251
  mcpToolPrefix = "";
2054
2252
  busyDeliveryMode = "none";
@@ -2182,17 +2380,34 @@ var CopilotDriver = class {
2182
2380
  };
2183
2381
 
2184
2382
  // 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";
2383
+ import { spawn as spawn4, spawnSync } from "child_process";
2384
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2187
2385
  import path6 from "path";
2188
2386
  var CursorDriver = class {
2189
2387
  id = "cursor";
2388
+ lifecycle = {
2389
+ kind: "per_turn",
2390
+ start: "immediate",
2391
+ exit: "natural",
2392
+ inFlightWake: "spawn_new"
2393
+ };
2394
+ communication = {
2395
+ chat: "mcp_chat_bridge",
2396
+ runtimeControl: "mcp_runtime_actions"
2397
+ };
2398
+ session = {
2399
+ recovery: "resume_or_fresh"
2400
+ };
2401
+ model = {
2402
+ detectedModelsVerifiedAs: "launchable",
2403
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2404
+ };
2190
2405
  supportsStdinNotification = false;
2191
2406
  mcpToolPrefix = "mcp__chat__";
2192
2407
  busyDeliveryMode = "none";
2193
2408
  spawn(ctx) {
2194
2409
  const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
2195
- if (!existsSync3(cursorDir)) {
2410
+ if (!existsSync4(cursorDir)) {
2196
2411
  mkdirSync2(cursorDir, { recursive: true });
2197
2412
  }
2198
2413
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
@@ -2299,49 +2514,89 @@ var CursorDriver = class {
2299
2514
  messageNotificationStyle: "poll"
2300
2515
  });
2301
2516
  }
2517
+ async detectModels() {
2518
+ return detectCursorModels();
2519
+ }
2302
2520
  };
2521
+ function parseCursorModelsOutput(output) {
2522
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
2523
+ const models = [];
2524
+ let defaultModel;
2525
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
2526
+ const line = rawLine.trim();
2527
+ if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
2528
+ if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
2529
+ let modelLine = line;
2530
+ const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
2531
+ const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
2532
+ if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
2533
+ const markerStart = markerMatch?.index ?? modelLine.length;
2534
+ modelLine = modelLine.slice(0, markerStart).trim();
2535
+ }
2536
+ const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
2537
+ if (!match) continue;
2538
+ const id = match[1]?.trim();
2539
+ if (!id || id.startsWith("-")) continue;
2540
+ const label = match[2]?.trim() || id;
2541
+ models.push({ id, label, verified: "launchable" });
2542
+ if (markers.includes("default")) defaultModel = id;
2543
+ }
2544
+ if (models.length === 0) return null;
2545
+ return { models, default: defaultModel };
2546
+ }
2547
+ function detectCursorModels(runCommand = runCursorModelsCommand) {
2548
+ const result = runCommand();
2549
+ if (result.error || result.status !== 0) return null;
2550
+ return parseCursorModelsOutput(String(result.stdout || ""));
2551
+ }
2552
+ function runCursorModelsCommand() {
2553
+ return spawnSync("cursor-agent", ["models"], {
2554
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
2555
+ encoding: "utf8",
2556
+ timeout: 5e3
2557
+ });
2558
+ }
2303
2559
 
2304
2560
  // src/drivers/gemini.ts
2305
2561
  import { spawn as spawn5 } from "child_process";
2306
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2562
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2307
2563
  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
- };
2564
+ function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2565
+ const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
2566
+ if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
2567
+ spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
2568
+ }
2569
+ return spawnEnv;
2318
2570
  }
2319
2571
  var GeminiDriver = class {
2320
2572
  id = "gemini";
2573
+ lifecycle = {
2574
+ kind: "per_turn",
2575
+ start: "immediate",
2576
+ exit: "natural",
2577
+ inFlightWake: "spawn_new"
2578
+ };
2579
+ communication = {
2580
+ chat: "slock_cli",
2581
+ runtimeControl: "mcp_runtime_actions"
2582
+ };
2583
+ session = {
2584
+ recovery: "resume_or_fresh"
2585
+ };
2586
+ model = {
2587
+ detectedModelsVerifiedAs: "suggestion_only",
2588
+ toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
2589
+ };
2321
2590
  supportsStdinNotification = false;
2322
2591
  mcpToolPrefix = "";
2323
2592
  busyDeliveryMode = "none";
2593
+ usesSlockCliForCommunication = true;
2324
2594
  sessionId = null;
2325
2595
  sessionAnnounced = false;
2326
2596
  spawn(ctx) {
2327
2597
  this.sessionId = ctx.config.sessionId || null;
2328
2598
  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");
2599
+ this.writeGeminiSettings(ctx);
2345
2600
  const args = [
2346
2601
  "--output-format",
2347
2602
  "stream-json",
@@ -2415,23 +2670,56 @@ var GeminiDriver = class {
2415
2670
  return null;
2416
2671
  }
2417
2672
  buildSystemPrompt(config, _agentId) {
2418
- return buildMcpSystemPrompt(config, {
2673
+ return buildCliTransportSystemPrompt(config, {
2419
2674
  toolPrefix: "",
2420
2675
  extraCriticalRules: [
2421
- "- Do NOT use shell commands to send or receive messages. The MCP tools handle everything."
2676
+ "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2677
+ ],
2678
+ postStartupNotes: [
2679
+ "**Gemini runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
2422
2680
  ],
2423
- postStartupNotes: [],
2424
2681
  includeStdinNotificationSection: false,
2425
2682
  messageNotificationStyle: "poll"
2426
2683
  });
2427
2684
  }
2685
+ writeGeminiSettings(ctx) {
2686
+ const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
2687
+ mkdirSync3(geminiDir, { recursive: true });
2688
+ const settingsPath = path7.join(geminiDir, "settings.json");
2689
+ writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
2690
+ }
2691
+ buildRuntimeActionsMcpSettings(ctx) {
2692
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2693
+ const command = isTsSource ? "npx" : "node";
2694
+ const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
2695
+ return {
2696
+ mcpServers: {
2697
+ chat: {
2698
+ command,
2699
+ args: [
2700
+ ...bridgeArgs,
2701
+ "--agent-id",
2702
+ ctx.agentId,
2703
+ "--server-url",
2704
+ ctx.config.serverUrl,
2705
+ "--auth-token",
2706
+ ctx.config.authToken || ctx.daemonApiKey,
2707
+ "--runtime",
2708
+ this.id,
2709
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2710
+ "--runtime-actions-only"
2711
+ ]
2712
+ }
2713
+ }
2714
+ };
2715
+ }
2428
2716
  };
2429
2717
 
2430
2718
  // src/drivers/kimi.ts
2431
2719
  import { randomUUID } from "crypto";
2432
2720
  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";
2721
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2722
+ import os3 from "os";
2435
2723
  import path8 from "path";
2436
2724
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2437
2725
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
@@ -2447,6 +2735,22 @@ function parseToolArguments(raw) {
2447
2735
  }
2448
2736
  var KimiDriver = class {
2449
2737
  id = "kimi";
2738
+ lifecycle = {
2739
+ kind: "persistent",
2740
+ stdin: "direct",
2741
+ inFlightWake: "steer"
2742
+ };
2743
+ communication = {
2744
+ chat: "mcp_chat_bridge",
2745
+ runtimeControl: "mcp_runtime_actions"
2746
+ };
2747
+ session = {
2748
+ recovery: "resume_or_fresh"
2749
+ };
2750
+ model = {
2751
+ detectedModelsVerifiedAs: "launchable",
2752
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2753
+ };
2450
2754
  supportsStdinNotification = true;
2451
2755
  mcpToolPrefix = "";
2452
2756
  busyDeliveryMode = "direct";
@@ -2631,11 +2935,11 @@ var KimiDriver = class {
2631
2935
  return detectKimiModels();
2632
2936
  }
2633
2937
  };
2634
- function detectKimiModels(home = os2.homedir()) {
2938
+ function detectKimiModels(home = os3.homedir()) {
2635
2939
  const configPath = path8.join(home, ".kimi", "config.toml");
2636
2940
  let raw;
2637
2941
  try {
2638
- raw = readFileSync2(configPath, "utf8");
2942
+ raw = readFileSync3(configPath, "utf8");
2639
2943
  } catch {
2640
2944
  return null;
2641
2945
  }
@@ -2647,7 +2951,7 @@ function detectKimiModels(home = os2.homedir()) {
2647
2951
  let key = match[1].trim();
2648
2952
  if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
2649
2953
  if (!key) continue;
2650
- models.push({ id: key, label: key });
2954
+ models.push({ id: key, label: key, verified: "launchable" });
2651
2955
  }
2652
2956
  void sectionRe;
2653
2957
  if (models.length === 0) return null;
@@ -2658,9 +2962,9 @@ function detectKimiModels(home = os2.homedir()) {
2658
2962
  }
2659
2963
 
2660
2964
  // src/drivers/opencode.ts
2661
- import { spawn as spawn7 } from "child_process";
2662
- import { readFileSync as readFileSync3 } from "fs";
2663
- import os3 from "os";
2965
+ import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
2966
+ import { readFileSync as readFileSync4 } from "fs";
2967
+ import os4 from "os";
2664
2968
  import path9 from "path";
2665
2969
  var CHAT_MCP_SERVER_NAME = "chat";
2666
2970
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
@@ -2700,10 +3004,10 @@ function parseUserOpenCodeConfig(ctx) {
2700
3004
  const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2701
3005
  return parseOpenCodeConfigContent(raw);
2702
3006
  }
2703
- function readLocalOpenCodeConfig(home = os3.homedir()) {
3007
+ function readLocalOpenCodeConfig(home = os4.homedir()) {
2704
3008
  const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2705
3009
  try {
2706
- return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
3010
+ return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
2707
3011
  } catch {
2708
3012
  }
2709
3013
  return {};
@@ -2749,7 +3053,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
2749
3053
  }
2750
3054
  };
2751
3055
  }
2752
- function buildOpenCodeConfig(ctx, home = os3.homedir()) {
3056
+ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
2753
3057
  const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2754
3058
  const userAgents = recordField(userConfig.agent);
2755
3059
  const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
@@ -2774,7 +3078,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2774
3078
  }
2775
3079
  };
2776
3080
  }
2777
- function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
3081
+ function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
2778
3082
  const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2779
3083
  const config = buildOpenCodeConfig(ctx, home);
2780
3084
  const env = {
@@ -2801,21 +3105,42 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2801
3105
  args.push("--", turnPrompt);
2802
3106
  return { args, env, config };
2803
3107
  }
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
- }
3108
+ function parseOpenCodeModelsOutput(output) {
3109
+ const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
3110
+ const models = [];
3111
+ const seen = /* @__PURE__ */ new Set();
3112
+ for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
3113
+ const line = rawLine.trim();
3114
+ if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
3115
+ if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
3116
+ if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
3117
+ if (seen.has(line)) continue;
3118
+ seen.add(line);
3119
+ models.push({
3120
+ id: line,
3121
+ label: line,
3122
+ verified: "launchable"
3123
+ });
2816
3124
  }
2817
3125
  return models.length > 0 ? { models } : null;
2818
3126
  }
3127
+ function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3128
+ const commandResult = runCommand(home);
3129
+ if (commandResult.error || commandResult.status !== 0) return null;
3130
+ return parseOpenCodeModelsOutput(commandResult.stdout);
3131
+ }
3132
+ function runOpenCodeModelsCommand(home) {
3133
+ const result = spawnSync2("opencode", ["models"], {
3134
+ env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
3135
+ encoding: "utf8",
3136
+ timeout: 5e3
3137
+ });
3138
+ return {
3139
+ status: result.status,
3140
+ stdout: String(result.stdout || ""),
3141
+ error: result.error
3142
+ };
3143
+ }
2819
3144
  function isSystemFirstMessageTask(message) {
2820
3145
  return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2821
3146
  }
@@ -2834,6 +3159,38 @@ function buildOpenCodeSystemPrompt(config) {
2834
3159
  }
2835
3160
  var OpenCodeDriver = class {
2836
3161
  id = "opencode";
3162
+ lifecycle = {
3163
+ kind: "per_turn",
3164
+ start: "defer_until_concrete_message",
3165
+ exit: "terminate_on_turn_end",
3166
+ inFlightWake: "coalesce_into_pending"
3167
+ };
3168
+ communication = {
3169
+ chat: "slock_cli",
3170
+ runtimeControl: "mcp_runtime_actions"
3171
+ };
3172
+ session = {
3173
+ recovery: "resume_or_fresh"
3174
+ };
3175
+ model = {
3176
+ detectedModelsVerifiedAs: "launchable",
3177
+ toLaunchSpec: (modelId, ctx, opts) => {
3178
+ if (!ctx) return { args: ["--model", modelId] };
3179
+ const launchCtx = {
3180
+ ...ctx,
3181
+ config: {
3182
+ ...ctx.config,
3183
+ model: modelId
3184
+ }
3185
+ };
3186
+ const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
3187
+ return {
3188
+ args: launch.args,
3189
+ env: launch.env,
3190
+ config: launch.config
3191
+ };
3192
+ }
3193
+ };
2837
3194
  supportsStdinNotification = false;
2838
3195
  mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
2839
3196
  busyDeliveryMode = "none";
@@ -3059,7 +3416,53 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3059
3416
  }
3060
3417
 
3061
3418
  // src/agentProcessManager.ts
3062
- var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
3419
+ var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3420
+ var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3421
+ var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3422
+ var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
3423
+ var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
3424
+ var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
3425
+ ".md",
3426
+ ".txt",
3427
+ ".json",
3428
+ ".js",
3429
+ ".ts",
3430
+ ".jsx",
3431
+ ".tsx",
3432
+ ".yaml",
3433
+ ".yml",
3434
+ ".toml",
3435
+ ".log",
3436
+ ".csv",
3437
+ ".xml",
3438
+ ".html",
3439
+ ".css",
3440
+ ".sh",
3441
+ ".py"
3442
+ ]);
3443
+ var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
3444
+ ".apng": "image/apng",
3445
+ ".avif": "image/avif",
3446
+ ".gif": "image/gif",
3447
+ ".jpg": "image/jpeg",
3448
+ ".jpeg": "image/jpeg",
3449
+ ".png": "image/png",
3450
+ ".webp": "image/webp"
3451
+ };
3452
+ function readPositiveIntegerEnv(name, fallback) {
3453
+ const raw = process.env[name];
3454
+ if (!raw) return fallback;
3455
+ const parsed = Number(raw);
3456
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
3457
+ return Math.floor(parsed);
3458
+ }
3459
+ function readNonNegativeIntegerEnv(name, fallback) {
3460
+ const raw = process.env[name];
3461
+ if (!raw) return fallback;
3462
+ const parsed = Number(raw);
3463
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
3464
+ return Math.floor(parsed);
3465
+ }
3063
3466
  function toLocalTime(iso) {
3064
3467
  const d = new Date(iso);
3065
3468
  if (isNaN(d.getTime())) return iso;
@@ -3085,6 +3488,7 @@ function formatMessageTarget(message) {
3085
3488
  function getMessageShortId(messageId) {
3086
3489
  return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
3087
3490
  }
3491
+ var RESPONSE_TARGET_HINT = "Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.";
3088
3492
  function findSessionJsonl(root, predicate) {
3089
3493
  let visited = 0;
3090
3494
  const maxEntries = 1e4;
@@ -3093,7 +3497,7 @@ function findSessionJsonl(root, predicate) {
3093
3497
  if (depth < 0 || visited >= maxEntries) return null;
3094
3498
  let entries;
3095
3499
  try {
3096
- entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3500
+ entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3097
3501
  } catch {
3098
3502
  return null;
3099
3503
  }
@@ -3139,11 +3543,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3139
3543
  return null;
3140
3544
  }
3141
3545
  }
3142
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3546
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3143
3547
  const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
3144
3548
  if (directPath) {
3145
3549
  try {
3146
- if (statSync(directPath).isFile()) {
3550
+ if (statSync2(directPath).isFile()) {
3147
3551
  return { label: sessionId, path: directPath, runtime, reachable: true };
3148
3552
  }
3149
3553
  } catch {
@@ -3239,6 +3643,16 @@ function formatRuntimeProfileControlPrompt(messages) {
3239
3643
  return null;
3240
3644
  }
3241
3645
  const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
3646
+ const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
3647
+ if (!hasMigration) {
3648
+ return [
3649
+ "Runtime Profile daemon release notice.",
3650
+ "",
3651
+ "Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
3652
+ "",
3653
+ body
3654
+ ].join("\n");
3655
+ }
3242
3656
  return [
3243
3657
  "Runtime Profile control notice.",
3244
3658
  "",
@@ -3744,6 +4158,15 @@ function classifyTerminalFailure(ap) {
3744
4158
  }
3745
4159
  return null;
3746
4160
  }
4161
+ function hasDirectStdinRecoveryEvidence(ap) {
4162
+ const candidates = [
4163
+ ap.lastRuntimeError,
4164
+ ...ap.recentStderr
4165
+ ].filter((value) => !!value);
4166
+ return candidates.some(
4167
+ (text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
4168
+ );
4169
+ }
3747
4170
  function isMissingResumeSession(ap) {
3748
4171
  if (!ap.sessionId) return false;
3749
4172
  const candidates = [
@@ -3783,6 +4206,14 @@ var AgentProcessManager = class _AgentProcessManager {
3783
4206
  agents = /* @__PURE__ */ new Map();
3784
4207
  agentsStarting = /* @__PURE__ */ new Set();
3785
4208
  // Prevent concurrent starts of same agent
4209
+ queuedAgentStarts = /* @__PURE__ */ new Map();
4210
+ agentStartQueue = [];
4211
+ activeAgentStartCount = 0;
4212
+ agentStartPumpTimer = null;
4213
+ lastAgentStartAt = 0;
4214
+ lastAgentStartAgentId = null;
4215
+ maxConcurrentAgentStarts;
4216
+ agentStartIntervalMs;
3786
4217
  startingInboxes = /* @__PURE__ */ new Map();
3787
4218
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
3788
4219
  idleAgentConfigs = /* @__PURE__ */ new Map();
@@ -3796,6 +4227,7 @@ var AgentProcessManager = class _AgentProcessManager {
3796
4227
  driverResolver;
3797
4228
  defaultAgentEnvVarsProvider;
3798
4229
  tracer;
4230
+ deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
3799
4231
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
3800
4232
  this.chatBridgePath = chatBridgePath;
3801
4233
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -3803,59 +4235,286 @@ var AgentProcessManager = class _AgentProcessManager {
3803
4235
  this.daemonApiKey = daemonApiKey;
3804
4236
  this.serverUrl = opts.serverUrl;
3805
4237
  this.dataDir = opts.dataDir || DATA_DIR;
3806
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
4238
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
3807
4239
  this.driverResolver = opts.driverResolver || getDriver;
3808
4240
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3809
4241
  this.tracer = opts.tracer ?? noopTracer;
4242
+ this.maxConcurrentAgentStarts = Math.max(
4243
+ 1,
4244
+ Math.floor(
4245
+ opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
4246
+ )
4247
+ );
4248
+ this.agentStartIntervalMs = Math.max(
4249
+ 0,
4250
+ Math.floor(
4251
+ opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
4252
+ )
4253
+ );
4254
+ }
4255
+ setTracer(tracer) {
4256
+ this.tracer = tracer;
4257
+ }
4258
+ recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
4259
+ const span = this.tracer.startSpan(name, {
4260
+ parent: parseTraceparent(parentTraceparent),
4261
+ surface: "daemon",
4262
+ kind: "internal",
4263
+ attrs
4264
+ });
4265
+ span.end(status);
4266
+ }
4267
+ startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4268
+ return {
4269
+ agentId,
4270
+ launchId,
4271
+ runtime: config.runtime,
4272
+ model: config.model,
4273
+ session_id_present: Boolean(config.sessionId),
4274
+ launch_id_present: Boolean(launchId),
4275
+ wake_message_present: Boolean(wakeMessage),
4276
+ unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
4277
+ resume_prompt_present: Boolean(resumePrompt),
4278
+ queue_depth: this.agentStartQueue.length,
4279
+ active_starts: this.activeAgentStartCount,
4280
+ max_concurrent_starts: this.maxConcurrentAgentStarts,
4281
+ min_start_interval_ms: this.agentStartIntervalMs
4282
+ };
4283
+ }
4284
+ getDeliveryTraceContext(message) {
4285
+ return this.deliveryTraceContexts.get(message) ?? {};
4286
+ }
4287
+ deliveryTraceAttrs(agentId, message, attrs = {}) {
4288
+ const context = this.getDeliveryTraceContext(message);
4289
+ const deliveryCorrelationId = context.deliveryId ?? message.message_id;
4290
+ return {
4291
+ agentId,
4292
+ deliveryId: context.deliveryId,
4293
+ delivery_correlation_id: deliveryCorrelationId,
4294
+ channel_type: message.channel_type,
4295
+ sender_type: message.sender_type,
4296
+ messageId: message.message_id,
4297
+ message_id_present: Boolean(message.message_id),
4298
+ ...attrs
4299
+ };
3810
4300
  }
3811
4301
  async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4302
+ this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
3812
4303
  if (this.agents.has(agentId)) {
4304
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4305
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4306
+ reason: "already_running"
4307
+ });
3813
4308
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
3814
4309
  return;
3815
4310
  }
3816
4311
  if (this.agentsStarting.has(agentId)) {
4312
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4313
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4314
+ reason: "already_starting"
4315
+ });
3817
4316
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
3818
4317
  return;
3819
4318
  }
3820
- this.agentsStarting.add(agentId);
3821
- try {
3822
- const driver = this.driverResolver(config.runtime || "claude");
3823
- const agentDataDir = path11.join(this.dataDir, agentId);
3824
- await mkdir(agentDataDir, { recursive: true });
3825
- const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
3826
- const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
3827
- try {
3828
- await access(memoryMdPath);
3829
- } catch {
3830
- const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
3831
- await writeFile(memoryMdPath, initialMemoryMd);
3832
- }
3833
- const notesDir = path11.join(agentDataDir, "notes");
3834
- await mkdir(notesDir, { recursive: true });
3835
- if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
3836
- const seedFiles = buildOnboardingSeedFiles();
3837
- for (const { relativePath, content } of seedFiles) {
3838
- const fullPath = path11.join(agentDataDir, relativePath);
3839
- try {
3840
- await access(fullPath);
3841
- } catch {
3842
- await mkdir(path11.dirname(fullPath), { recursive: true });
3843
- await writeFile(fullPath, content);
3844
- }
3845
- }
3846
- }
3847
- const isResume = !!runtimeConfig.sessionId;
3848
- const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
3849
- let prompt;
3850
- if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
3851
- prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
3852
- } else if (isResume && resumePrompt) {
3853
- prompt = resumePrompt;
3854
- prompt += getBusyDeliveryNote(driver);
3855
- } else if (wakeMessage) {
3856
- const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
3857
- const channelLabel = formatChannelLabel(wakeMessage);
3858
- prompt = runtimeProfileControlPrompt ?? `New message received:
4319
+ if (this.queuedAgentStarts.has(agentId)) {
4320
+ this.recordDaemonTrace("daemon.agent.start.ignored", {
4321
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4322
+ reason: "already_queued"
4323
+ });
4324
+ logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
4325
+ return;
4326
+ }
4327
+ return new Promise((resolve, reject) => {
4328
+ const item = {
4329
+ agentId,
4330
+ config,
4331
+ wakeMessage,
4332
+ unreadSummary,
4333
+ resumePrompt,
4334
+ launchId,
4335
+ resolve,
4336
+ reject
4337
+ };
4338
+ this.agentStartQueue.push(item);
4339
+ this.queuedAgentStarts.set(agentId, item);
4340
+ this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4341
+ logger.info(
4342
+ `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
4343
+ );
4344
+ this.pumpAgentStartQueue();
4345
+ });
4346
+ }
4347
+ pumpAgentStartQueue() {
4348
+ if (this.agentStartPumpTimer) return;
4349
+ if (this.agentStartQueue.length === 0) return;
4350
+ if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
4351
+ const next = this.agentStartQueue[0];
4352
+ const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
4353
+ const elapsed = Date.now() - this.lastAgentStartAt;
4354
+ const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
4355
+ if (waitMs > 0) {
4356
+ this.recordDaemonTrace("daemon.agent.start.rate_limited", {
4357
+ ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
4358
+ wait_ms: waitMs
4359
+ });
4360
+ this.agentStartPumpTimer = setTimeout(() => {
4361
+ this.agentStartPumpTimer = null;
4362
+ this.pumpAgentStartQueue();
4363
+ }, waitMs);
4364
+ return;
4365
+ }
4366
+ const item = this.agentStartQueue.shift();
4367
+ if (!item) return;
4368
+ if (this.queuedAgentStarts.get(item.agentId) !== item) {
4369
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4370
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4371
+ reason: "stale_queue_item"
4372
+ });
4373
+ this.pumpAgentStartQueue();
4374
+ return;
4375
+ }
4376
+ this.queuedAgentStarts.delete(item.agentId);
4377
+ if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
4378
+ this.recordDaemonTrace("daemon.agent.start.skipped", {
4379
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4380
+ reason: "already_running_or_starting"
4381
+ });
4382
+ logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
4383
+ item.resolve();
4384
+ this.pumpAgentStartQueue();
4385
+ return;
4386
+ }
4387
+ this.activeAgentStartCount++;
4388
+ this.lastAgentStartAt = Date.now();
4389
+ this.lastAgentStartAgentId = item.agentId;
4390
+ logger.info(
4391
+ `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
4392
+ );
4393
+ this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
4394
+ this.startAgentNow(
4395
+ item.agentId,
4396
+ item.config,
4397
+ item.wakeMessage,
4398
+ item.unreadSummary,
4399
+ item.resumePrompt,
4400
+ item.launchId
4401
+ ).then(() => {
4402
+ this.releaseAgentStartSlot(item.agentId, "spawn attempted");
4403
+ item.resolve();
4404
+ }, (err) => {
4405
+ this.releaseAgentStartSlot(item.agentId, "start failed");
4406
+ item.reject(err);
4407
+ });
4408
+ }
4409
+ releaseAgentStartSlot(agentId, reason) {
4410
+ if (this.activeAgentStartCount <= 0) return;
4411
+ this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
4412
+ this.recordDaemonTrace("daemon.agent.start.slot_released", {
4413
+ agentId,
4414
+ reason,
4415
+ active_starts: this.activeAgentStartCount,
4416
+ queue_depth: this.agentStartQueue.length,
4417
+ max_concurrent_starts: this.maxConcurrentAgentStarts
4418
+ });
4419
+ logger.info(
4420
+ `[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
4421
+ );
4422
+ this.pumpAgentStartQueue();
4423
+ }
4424
+ cancelQueuedAgentStart(agentId, reason) {
4425
+ const item = this.queuedAgentStarts.get(agentId);
4426
+ if (!item) return false;
4427
+ this.queuedAgentStarts.delete(agentId);
4428
+ this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
4429
+ this.startingInboxes.delete(agentId);
4430
+ if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
4431
+ clearTimeout(this.agentStartPumpTimer);
4432
+ this.agentStartPumpTimer = null;
4433
+ }
4434
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4435
+ ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4436
+ reason
4437
+ }, "cancelled");
4438
+ logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
4439
+ item.resolve();
4440
+ return true;
4441
+ }
4442
+ cancelAllQueuedAgentStarts(reason) {
4443
+ for (const item of this.agentStartQueue) {
4444
+ if (this.queuedAgentStarts.get(item.agentId) === item) {
4445
+ this.recordDaemonTrace("daemon.agent.start.cancelled", {
4446
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
4447
+ reason
4448
+ }, "cancelled");
4449
+ logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
4450
+ item.resolve();
4451
+ }
4452
+ }
4453
+ this.agentStartQueue = [];
4454
+ this.queuedAgentStarts.clear();
4455
+ this.startingInboxes.clear();
4456
+ if (this.agentStartPumpTimer) {
4457
+ clearTimeout(this.agentStartPumpTimer);
4458
+ this.agentStartPumpTimer = null;
4459
+ }
4460
+ }
4461
+ async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
4462
+ if (this.agents.has(agentId)) {
4463
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4464
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4465
+ reason: "already_running"
4466
+ });
4467
+ logger.info(`[Agent ${agentId}] Start ignored (already running)`);
4468
+ return;
4469
+ }
4470
+ if (this.agentsStarting.has(agentId)) {
4471
+ this.recordDaemonTrace("daemon.agent.spawn.skipped", {
4472
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4473
+ reason: "already_starting"
4474
+ });
4475
+ logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
4476
+ return;
4477
+ }
4478
+ this.agentsStarting.add(agentId);
4479
+ this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4480
+ try {
4481
+ const driver = this.driverResolver(config.runtime || "claude");
4482
+ const agentDataDir = path11.join(this.dataDir, agentId);
4483
+ await mkdir(agentDataDir, { recursive: true });
4484
+ const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
4485
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
4486
+ try {
4487
+ await access(memoryMdPath);
4488
+ } catch {
4489
+ const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
4490
+ await writeFile(memoryMdPath, initialMemoryMd);
4491
+ }
4492
+ const notesDir = path11.join(agentDataDir, "notes");
4493
+ await mkdir(notesDir, { recursive: true });
4494
+ if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
4495
+ const seedFiles = buildOnboardingSeedFiles();
4496
+ for (const { relativePath, content } of seedFiles) {
4497
+ const fullPath = path11.join(agentDataDir, relativePath);
4498
+ try {
4499
+ await access(fullPath);
4500
+ } catch {
4501
+ await mkdir(path11.dirname(fullPath), { recursive: true });
4502
+ await writeFile(fullPath, content);
4503
+ }
4504
+ }
4505
+ }
4506
+ const isResume = !!runtimeConfig.sessionId;
4507
+ const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
4508
+ let prompt;
4509
+ if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
4510
+ prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
4511
+ } else if (isResume && resumePrompt) {
4512
+ prompt = resumePrompt;
4513
+ prompt += getBusyDeliveryNote(driver);
4514
+ } else if (wakeMessage) {
4515
+ const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
4516
+ const channelLabel = formatChannelLabel(wakeMessage);
4517
+ prompt = runtimeProfileControlPrompt ?? `New message received:
3859
4518
 
3860
4519
  ${formatIncomingMessage(wakeMessage, driver)}`;
3861
4520
  if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
@@ -3877,6 +4536,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
3877
4536
  prompt += `
3878
4537
 
3879
4538
  Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
4539
+ ${RESPONSE_TARGET_HINT}
3880
4540
 
3881
4541
  IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
3882
4542
  prompt += getBusyDeliveryNote(driver);
@@ -3909,6 +4569,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3909
4569
  });
3910
4570
  this.sendAgentStatus(agentId, "active", launchId || null);
3911
4571
  this.broadcastActivity(agentId, "online", "Process idle");
4572
+ this.recordDaemonTrace("daemon.agent.spawn.deferred", {
4573
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
4574
+ pending_messages_count: pendingMessages.length,
4575
+ reason: "defer_until_concrete_message"
4576
+ });
3912
4577
  logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
3913
4578
  for (const message of pendingMessages) {
3914
4579
  this.deliverMessage(agentId, message);
@@ -3926,6 +4591,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3926
4591
  daemonApiKey: this.daemonApiKey,
3927
4592
  launchId: launchId || null
3928
4593
  });
4594
+ this.recordDaemonTrace("daemon.agent.spawn.created", {
4595
+ ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
4596
+ detached: false,
4597
+ new_session: false,
4598
+ process_pid_present: typeof proc.pid === "number"
4599
+ });
3929
4600
  const agentProcess = {
3930
4601
  process: proc,
3931
4602
  driver,
@@ -3947,6 +4618,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3947
4618
  runtimeTraceSpan: null,
3948
4619
  lastActivity: "",
3949
4620
  lastActivityDetail: "",
4621
+ activityClientSeq: 0,
3950
4622
  recentStdout: [],
3951
4623
  recentStderr: [],
3952
4624
  lastRuntimeError: null,
@@ -3959,7 +4631,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3959
4631
  };
3960
4632
  this.startingInboxes.delete(agentId);
3961
4633
  this.agents.set(agentId, agentProcess);
3962
- this.startRuntimeTrace(agentId, agentProcess, "spawn");
4634
+ this.idleAgentConfigs.set(agentId, {
4635
+ config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
4636
+ sessionId: effectiveConfig.sessionId || null,
4637
+ launchId: launchId || null
4638
+ });
4639
+ this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
3963
4640
  this.agentsStarting.delete(agentId);
3964
4641
  if (config.runtimeProfileControl) {
3965
4642
  this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
@@ -3998,6 +4675,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
3998
4675
  proc.on("error", (err) => {
3999
4676
  const current = this.agents.get(agentId);
4000
4677
  if (current) current.spawnError = err.message;
4678
+ this.recordDaemonTrace("daemon.agent.process.error", {
4679
+ agentId,
4680
+ launchId: current?.launchId || void 0,
4681
+ runtime: config.runtime,
4682
+ model: config.model,
4683
+ error_class: err.name || typeof err
4684
+ }, "error");
4001
4685
  logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
4002
4686
  });
4003
4687
  proc.on("exit", (code, signal) => {
@@ -4006,6 +4690,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4006
4690
  current.exitCode = code;
4007
4691
  current.exitSignal = signal;
4008
4692
  }
4693
+ this.recordDaemonTrace("daemon.agent.process.exited", {
4694
+ agentId,
4695
+ launchId: current?.launchId || void 0,
4696
+ runtime: config.runtime,
4697
+ model: config.model,
4698
+ exit_code: code,
4699
+ exit_signal: signal,
4700
+ clean_exit: code === 0,
4701
+ runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4702
+ inbox_count: current?.inbox.length ?? 0,
4703
+ pending_notification_count: current?.pendingNotificationCount ?? 0
4704
+ });
4009
4705
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4010
4706
  });
4011
4707
  proc.on("close", (code, signal) => {
@@ -4069,7 +4765,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4069
4765
  }
4070
4766
  if (processEndedCleanly) {
4071
4767
  let queuedWakeMessage;
4072
- if (!ap.driver.supportsStdinNotification) {
4768
+ if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
4073
4769
  while (ap.inbox.length > 0) {
4074
4770
  const candidate = ap.inbox.shift();
4075
4771
  if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
@@ -4175,7 +4871,68 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4175
4871
  }
4176
4872
  return leftKeys.every((key) => left?.[key] === right?.[key]);
4177
4873
  }
4874
+ enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
4875
+ ap.inbox.push(message);
4876
+ if (ap.driver.supportsStdinNotification && ap.sessionId) {
4877
+ ap.pendingNotificationCount++;
4878
+ if (ap.driver.busyDeliveryMode === "gated") {
4879
+ this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4880
+ reason: "runtime_profile",
4881
+ kind,
4882
+ pendingMessages: ap.inbox.length
4883
+ });
4884
+ } else if (!ap.notificationTimer) {
4885
+ ap.notificationTimer = setTimeout(() => {
4886
+ this.sendStdinNotification(agentId);
4887
+ }, 3e3);
4888
+ }
4889
+ }
4890
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
4891
+ agentId,
4892
+ kind,
4893
+ key_present: Boolean(key),
4894
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
4895
+ runtime: ap.config.runtime,
4896
+ session_id_present: Boolean(ap.sessionId),
4897
+ launchId: ap.launchId || void 0,
4898
+ inbox_count: ap.inbox.length,
4899
+ pending_notification_count: ap.pendingNotificationCount,
4900
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
4901
+ supports_stdin_notification: ap.driver.supportsStdinNotification
4902
+ });
4903
+ logger.info(
4904
+ `[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
4905
+ );
4906
+ }
4907
+ queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
4908
+ const pending = this.startingInboxes.get(agentId) || [];
4909
+ pending.push(message);
4910
+ this.startingInboxes.set(agentId, pending);
4911
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4912
+ this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
4913
+ agentId,
4914
+ kind,
4915
+ key_present: Boolean(key),
4916
+ outcome: "queued_during_start",
4917
+ startup_pending: true,
4918
+ starting_inbox_count: pending.length,
4919
+ launchId: queuedStart?.launchId
4920
+ });
4921
+ logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
4922
+ }
4923
+ splitRuntimeProfileControlBatch(messages) {
4924
+ const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
4925
+ if (controlMessages.length === 0 || controlMessages.length === messages.length) {
4926
+ return { nextMessages: messages, deferredMessages: [] };
4927
+ }
4928
+ const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
4929
+ return { nextMessages: controlMessages, deferredMessages };
4930
+ }
4931
+ containsOrdinaryInboxMessage(messages) {
4932
+ return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
4933
+ }
4178
4934
  async stopAgent(agentId, { wait = false, silent = false } = {}) {
4935
+ this.cancelQueuedAgentStart(agentId, "stop requested");
4179
4936
  this.idleAgentConfigs.delete(agentId);
4180
4937
  const ap = this.agents.get(agentId);
4181
4938
  if (!ap) {
@@ -4222,51 +4979,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4222
4979
  });
4223
4980
  }
4224
4981
  }
4225
- deliverMessage(agentId, message) {
4982
+ deliverMessage(agentId, message, traceContext = {}) {
4983
+ if (traceContext.deliveryId) {
4984
+ this.deliveryTraceContexts.set(message, traceContext);
4985
+ }
4226
4986
  const ap = this.agents.get(agentId);
4227
4987
  if (!ap) {
4228
- if (this.agentsStarting.has(agentId)) {
4988
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
4989
+ const queuedStart = this.queuedAgentStarts.get(agentId);
4229
4990
  const pending = this.startingInboxes.get(agentId) || [];
4230
4991
  pending.push(message);
4231
4992
  this.startingInboxes.set(agentId, pending);
4232
- return;
4993
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
4994
+ outcome: "queued_during_start",
4995
+ accepted: true,
4996
+ process_present: false,
4997
+ startup_pending: true,
4998
+ starting_inbox_count: pending.length,
4999
+ launchId: queuedStart?.launchId
5000
+ }));
5001
+ return true;
4233
5002
  }
4234
5003
  const cached = this.idleAgentConfigs.get(agentId);
4235
5004
  if (cached) {
4236
5005
  const driver = this.driverResolver(cached.config.runtime || "claude");
4237
5006
  if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4238
- return;
5007
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5008
+ outcome: "deferred_wake_message",
5009
+ accepted: true,
5010
+ process_present: false,
5011
+ cached_idle_config_present: true,
5012
+ runtime: cached.config.runtime,
5013
+ session_id_present: Boolean(cached.sessionId),
5014
+ launchId: cached.launchId || void 0
5015
+ }));
5016
+ return true;
4239
5017
  }
4240
5018
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
4241
5019
  this.idleAgentConfigs.delete(agentId);
5020
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5021
+ outcome: "auto_restart_from_idle",
5022
+ accepted: true,
5023
+ process_present: false,
5024
+ cached_idle_config_present: true,
5025
+ runtime: cached.config.runtime,
5026
+ session_id_present: Boolean(cached.sessionId),
5027
+ launchId: cached.launchId || void 0
5028
+ }));
4242
5029
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
4243
5030
  logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
4244
5031
  });
5032
+ return true;
4245
5033
  }
4246
- return;
5034
+ logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
5035
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5036
+ outcome: "rejected_no_process",
5037
+ accepted: false,
5038
+ process_present: false,
5039
+ cached_idle_config_present: false
5040
+ }), "error");
5041
+ this.sendAgentStatus(agentId, "inactive", null);
5042
+ this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
5043
+ return false;
4247
5044
  }
4248
5045
  if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4249
- return;
5046
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5047
+ outcome: "deferred_wake_message",
5048
+ accepted: true,
5049
+ process_present: true,
5050
+ runtime: ap.config.runtime,
5051
+ session_id_present: Boolean(ap.sessionId),
5052
+ launchId: ap.launchId || void 0,
5053
+ is_idle: ap.isIdle,
5054
+ inbox_count: ap.inbox.length
5055
+ }));
5056
+ return true;
4250
5057
  }
4251
5058
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
4252
5059
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
4253
5060
  nextMessages.push(message);
4254
5061
  ap.isIdle = false;
4255
- this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
5062
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
4256
5063
  this.broadcastActivity(agentId, "working", "Message received");
4257
- this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
4258
- return;
5064
+ const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
5065
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5066
+ outcome: "stdin_idle_delivery",
5067
+ accepted: true,
5068
+ process_present: true,
5069
+ runtime: ap.config.runtime,
5070
+ session_id_present: true,
5071
+ launchId: ap.launchId || void 0,
5072
+ stdin_delivery_accepted: stdinAccepted,
5073
+ delivered_messages_count: nextMessages.length
5074
+ }));
5075
+ return true;
4259
5076
  }
4260
5077
  ap.inbox.push(message);
4261
- if (!ap.driver.supportsStdinNotification) return;
4262
- if (!ap.sessionId) return;
5078
+ if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
5079
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5080
+ outcome: "queued_stalled_recovery",
5081
+ accepted: true,
5082
+ process_present: true,
5083
+ runtime: ap.config.runtime,
5084
+ session_id_present: Boolean(ap.sessionId),
5085
+ launchId: ap.launchId || void 0,
5086
+ inbox_count: ap.inbox.length
5087
+ }));
5088
+ return true;
5089
+ }
5090
+ if (!ap.driver.supportsStdinNotification) {
5091
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5092
+ outcome: "queued_busy_non_stdin",
5093
+ accepted: true,
5094
+ process_present: true,
5095
+ runtime: ap.config.runtime,
5096
+ session_id_present: Boolean(ap.sessionId),
5097
+ launchId: ap.launchId || void 0,
5098
+ inbox_count: ap.inbox.length
5099
+ }));
5100
+ return true;
5101
+ }
5102
+ if (!ap.sessionId) {
5103
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5104
+ outcome: "queued_before_session",
5105
+ accepted: true,
5106
+ process_present: true,
5107
+ runtime: ap.config.runtime,
5108
+ session_id_present: false,
5109
+ launchId: ap.launchId || void 0,
5110
+ inbox_count: ap.inbox.length
5111
+ }));
5112
+ return true;
5113
+ }
4263
5114
  if (ap.driver.busyDeliveryMode === "gated") {
4264
5115
  ap.pendingNotificationCount++;
4265
5116
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
4266
5117
  reason: "busy_message",
4267
5118
  pendingMessages: ap.inbox.length
4268
5119
  });
4269
- return;
5120
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5121
+ outcome: "queued_busy_gated",
5122
+ accepted: true,
5123
+ process_present: true,
5124
+ runtime: ap.config.runtime,
5125
+ session_id_present: true,
5126
+ launchId: ap.launchId || void 0,
5127
+ inbox_count: ap.inbox.length,
5128
+ pending_notification_count: ap.pendingNotificationCount
5129
+ }));
5130
+ return true;
4270
5131
  }
4271
5132
  ap.pendingNotificationCount++;
4272
5133
  if (!ap.notificationTimer) {
@@ -4274,6 +5135,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4274
5135
  this.sendStdinNotification(agentId);
4275
5136
  }, 3e3);
4276
5137
  }
5138
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
5139
+ outcome: "queued_busy_notification",
5140
+ accepted: true,
5141
+ process_present: true,
5142
+ runtime: ap.config.runtime,
5143
+ session_id_present: true,
5144
+ inbox_count: ap.inbox.length,
5145
+ pending_notification_count: ap.pendingNotificationCount,
5146
+ notification_timer_present: Boolean(ap.notificationTimer)
5147
+ }));
5148
+ return true;
4277
5149
  }
4278
5150
  async resetWorkspace(agentId) {
4279
5151
  const agentDataDir = path11.join(this.dataDir, agentId);
@@ -4285,6 +5157,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4285
5157
  }
4286
5158
  }
4287
5159
  async stopAll() {
5160
+ this.cancelAllQueuedAgentStarts("daemon shutdown");
4288
5161
  this.idleAgentConfigs.clear();
4289
5162
  const ids = [...this.agents.keys()];
4290
5163
  await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
@@ -4306,6 +5179,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4306
5179
  getIdleAgentSessionIds() {
4307
5180
  const result = [];
4308
5181
  for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
5182
+ if (this.agents.has(agentId)) continue;
4309
5183
  if (sessionId) result.push({ agentId, sessionId, launchId });
4310
5184
  }
4311
5185
  return result;
@@ -4357,7 +5231,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4357
5231
  }
4358
5232
  return reports;
4359
5233
  }
4360
- deliverRuntimeProfileNotification(agentId, key, kind, content) {
5234
+ deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
5235
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5236
+ parent: parseTraceparent(traceparent),
5237
+ surface: "daemon",
5238
+ kind: "consumer",
5239
+ attrs: {
5240
+ agentId,
5241
+ control_kind: kind,
5242
+ key_present: Boolean(key)
5243
+ }
5244
+ });
4361
5245
  const now = (/* @__PURE__ */ new Date()).toISOString();
4362
5246
  const message = {
4363
5247
  channel_id: "system",
@@ -4368,18 +5252,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4368
5252
  sender_type: "system",
4369
5253
  content,
4370
5254
  timestamp: now,
4371
- message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
5255
+ message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
5256
+ traceparent: formatTraceparent(span.context)
4372
5257
  };
4373
5258
  const ap = this.agents.get(agentId);
5259
+ if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
5260
+ this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
5261
+ span.end("ok", {
5262
+ attrs: {
5263
+ outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5264
+ runtime: ap.config.runtime,
5265
+ launchId: ap.launchId || void 0,
5266
+ session_id_present: Boolean(ap.sessionId),
5267
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5268
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5269
+ }
5270
+ });
5271
+ return true;
5272
+ }
4374
5273
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
4375
5274
  ap.isIdle = false;
4376
5275
  this.startRuntimeTrace(agentId, ap, "runtime-profile");
4377
- this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
4378
- return;
5276
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5277
+ span.end(written ? "ok" : "error", {
5278
+ attrs: {
5279
+ outcome: written ? "stdin_idle" : "stdin_failed",
5280
+ runtime: ap.config.runtime,
5281
+ launchId: ap.launchId || void 0,
5282
+ session_id_present: true,
5283
+ supports_stdin_notification: true,
5284
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5285
+ }
5286
+ });
5287
+ return written;
4379
5288
  }
4380
5289
  if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
4381
- this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
4382
- return;
5290
+ const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
5291
+ span.end(written ? "ok" : "error", {
5292
+ attrs: {
5293
+ outcome: written ? "stdin_busy" : "stdin_failed",
5294
+ runtime: ap.config.runtime,
5295
+ launchId: ap.launchId || void 0,
5296
+ session_id_present: true,
5297
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
5298
+ busy_delivery_mode: ap.driver.busyDeliveryMode
5299
+ }
5300
+ });
5301
+ return written;
5302
+ }
5303
+ if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
5304
+ const queuedStart = this.queuedAgentStarts.get(agentId);
5305
+ this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
5306
+ span.end("ok", {
5307
+ attrs: {
5308
+ outcome: "queued_during_start",
5309
+ startup_pending: true,
5310
+ launchId: queuedStart?.launchId
5311
+ }
5312
+ });
5313
+ return true;
4383
5314
  }
4384
5315
  const cached = this.idleAgentConfigs.get(agentId);
4385
5316
  if (cached) {
@@ -4389,9 +5320,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4389
5320
  logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
4390
5321
  this.idleAgentConfigs.set(agentId, cached);
4391
5322
  });
4392
- return;
5323
+ span.end("ok", {
5324
+ attrs: {
5325
+ outcome: "restart_queued",
5326
+ runtime: cached.config.runtime,
5327
+ launchId: cached.launchId || void 0,
5328
+ session_id_present: Boolean(cached.sessionId)
5329
+ }
5330
+ });
5331
+ return true;
4393
5332
  }
4394
5333
  logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
5334
+ span.end("ok", { attrs: { outcome: "no_path" } });
5335
+ return false;
4395
5336
  }
4396
5337
  ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
4397
5338
  for (const message of messages) {
@@ -4404,19 +5345,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4404
5345
  type: "agent:runtime_profile:migration:ack",
4405
5346
  agentId,
4406
5347
  migrationKey: notification.key,
4407
- launchId: launchId || void 0
5348
+ launchId: launchId || void 0,
5349
+ traceparent: message.traceparent
4408
5350
  });
4409
5351
  } else {
4410
5352
  this.sendToServer({
4411
5353
  type: "agent:runtime_profile:daemon_release_notice:ack",
4412
5354
  agentId,
4413
5355
  noticeKey: notification.key,
4414
- launchId: launchId || void 0
5356
+ launchId: launchId || void 0,
5357
+ traceparent: message.traceparent
4415
5358
  });
4416
5359
  }
4417
5360
  }
4418
5361
  }
4419
5362
  ackInjectedRuntimeProfileControl(agentId, control, launchId) {
5363
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
5364
+ surface: "daemon",
5365
+ kind: "internal",
5366
+ attrs: {
5367
+ agentId,
5368
+ control_kind: control.kind,
5369
+ key_present: Boolean(control.key),
5370
+ launchId: launchId || void 0,
5371
+ source: "agent_config"
5372
+ }
5373
+ });
4420
5374
  const title = runtimeProfileNotificationTitle(control.kind);
4421
5375
  this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
4422
5376
  if (control.kind === "migration") {
@@ -4424,24 +5378,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4424
5378
  type: "agent:runtime_profile:migration:ack",
4425
5379
  agentId,
4426
5380
  migrationKey: control.key,
4427
- launchId: launchId || void 0
5381
+ launchId: launchId || void 0,
5382
+ traceparent: formatTraceparent(span.context)
4428
5383
  });
4429
5384
  } else {
4430
5385
  this.sendToServer({
4431
5386
  type: "agent:runtime_profile:daemon_release_notice:ack",
4432
5387
  agentId,
4433
5388
  noticeKey: control.key,
4434
- launchId: launchId || void 0
5389
+ launchId: launchId || void 0,
5390
+ traceparent: formatTraceparent(span.context)
4435
5391
  });
4436
5392
  }
5393
+ span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
4437
5394
  }
4438
5395
  sendRuntimeProfileWireReport(report) {
5396
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
5397
+ surface: "daemon",
5398
+ kind: "producer",
5399
+ attrs: {
5400
+ agentId: report.agentId,
5401
+ launchId: report.launchId || void 0,
5402
+ runtime: report.facts.runtime,
5403
+ model_present: Boolean(report.facts.model),
5404
+ session_ref_present: Boolean(report.facts.sessionRef),
5405
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
5406
+ }
5407
+ });
4439
5408
  this.sendToServer({
4440
5409
  type: "agent:runtime_profile",
4441
5410
  agentId: report.agentId,
4442
5411
  facts: report.facts,
4443
- launchId: report.launchId || void 0
5412
+ launchId: report.launchId || void 0,
5413
+ traceparent: formatTraceparent(span.context)
4444
5414
  });
5415
+ span.end("ok");
4445
5416
  }
4446
5417
  sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
4447
5418
  this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
@@ -4484,32 +5455,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4484
5455
  }
4485
5456
  const info = await stat2(resolved);
4486
5457
  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
5458
  const ext = path11.extname(resolved).toLowerCase();
4507
- if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4508
- return { content: null, binary: true };
5459
+ if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5460
+ if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5461
+ const content = await readFile(resolved, "utf-8");
5462
+ return { content, binary: false, size: info.size, encoding: "utf-8" };
5463
+ }
5464
+ const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
5465
+ if (imageMimeType) {
5466
+ if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
5467
+ return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
5468
+ }
5469
+ const content = await readFile(resolved, "base64");
5470
+ return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
4509
5471
  }
4510
- if (info.size > 1048576) throw new Error("File too large");
4511
- const content = await readFile(resolved, "utf-8");
4512
- return { content, binary: false };
5472
+ return { content: null, binary: true, size: info.size };
4513
5473
  }
4514
5474
  // Skill scanning
4515
5475
  // Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
@@ -4529,7 +5489,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4529
5489
  async listSkills(agentId, runtimeHint) {
4530
5490
  const agent = this.agents.get(agentId);
4531
5491
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4532
- const home = os4.homedir();
5492
+ const home = os5.homedir();
4533
5493
  const workspaceDir = path11.join(this.dataDir, agentId);
4534
5494
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4535
5495
  const globalResults = await Promise.all(
@@ -4619,13 +5579,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4619
5579
  if (!hasToolStart) {
4620
5580
  entries.push({ kind: "status", activity, detail });
4621
5581
  }
5582
+ if (ap) ap.activityClientSeq += 1;
4622
5583
  this.sendToServer({
4623
5584
  type: "agent:activity",
4624
5585
  agentId,
4625
5586
  activity,
4626
5587
  detail,
4627
5588
  entries,
4628
- launchId: launchIdOverride || ap?.launchId || void 0
5589
+ launchId: launchIdOverride || ap?.launchId || void 0,
5590
+ clientSeq: ap?.activityClientSeq
4629
5591
  });
4630
5592
  if (ap) {
4631
5593
  ap.lastActivity = activity;
@@ -4637,12 +5599,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4637
5599
  this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
4638
5600
  activity: ap.lastActivity
4639
5601
  });
5602
+ ap.activityClientSeq += 1;
4640
5603
  this.sendToServer({
4641
5604
  type: "agent:activity",
4642
5605
  agentId,
4643
5606
  activity: ap.lastActivity,
4644
5607
  detail: ap.lastActivityDetail,
4645
- launchId: launchIdOverride || ap.launchId || void 0
5608
+ launchId: launchIdOverride || ap.launchId || void 0,
5609
+ clientSeq: ap.activityClientSeq
4646
5610
  });
4647
5611
  }, ACTIVITY_HEARTBEAT_MS);
4648
5612
  }
@@ -4654,6 +5618,42 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4654
5618
  }
4655
5619
  }
4656
5620
  }
5621
+ /**
5622
+ * Respond to a server-issued `agent:activity_probe`. Echoes the
5623
+ * agent's current `lastActivity` back through the existing
5624
+ * `agent:activity` upstream channel with the matching `probeId`.
5625
+ *
5626
+ * Why this exists: the server's stale-activity sweep used to
5627
+ * synthesize `online` whenever a transient state went 90s without
5628
+ * an update. That invented state without consulting ground truth
5629
+ * and produced "agent shows green/idle but is actually working" UI
5630
+ * staleness (#engineering:72283cf7 task #340 RCA).
5631
+ *
5632
+ * The new flow: server sends `agent:activity_probe` for stale
5633
+ * agents, daemon replies here with the *real* current activity, and
5634
+ * the server only falls back to synth-online if the probe times out
5635
+ * (5s). The body is intentionally minimal — no entries, no
5636
+ * heartbeat side-effects, no state mutation. We just echo what we
5637
+ * already know.
5638
+ *
5639
+ * If the agent is no longer running locally (`ap` undefined), we
5640
+ * report `offline` so the server stops believing the agent is busy.
5641
+ */
5642
+ respondToActivityProbe(agentId, probeId) {
5643
+ const ap = this.agents.get(agentId);
5644
+ const activity = ap?.lastActivity || "offline";
5645
+ const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
5646
+ if (ap) ap.activityClientSeq += 1;
5647
+ this.sendToServer({
5648
+ type: "agent:activity",
5649
+ agentId,
5650
+ activity,
5651
+ detail,
5652
+ launchId: ap?.launchId || void 0,
5653
+ probeId,
5654
+ clientSeq: ap?.activityClientSeq
5655
+ });
5656
+ }
4657
5657
  flushPendingTrajectory(agentId) {
4658
5658
  const ap = this.agents.get(agentId);
4659
5659
  const pending = ap?.pendingTrajectory;
@@ -4719,7 +5719,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4719
5719
  this.clearCompactionWatchdog(ap);
4720
5720
  this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
4721
5721
  }
4722
- startRuntimeTrace(agentId, ap, reason) {
5722
+ messagesTraceAttrs(messages) {
5723
+ if (!messages || messages.length === 0) return {};
5724
+ const first = messages[0];
5725
+ const context = this.getDeliveryTraceContext(first);
5726
+ const notification = runtimeProfileNotificationFromMessage(first);
5727
+ if (notification) {
5728
+ return {
5729
+ messages_count: messages.length,
5730
+ message_id_present: Boolean(first.message_id),
5731
+ deliveryId: context.deliveryId,
5732
+ delivery_correlation_id: context.deliveryId,
5733
+ control_kind: notification.kind,
5734
+ key_present: Boolean(notification.key)
5735
+ };
5736
+ }
5737
+ return {
5738
+ messages_count: messages.length,
5739
+ messageId: first.message_id,
5740
+ message_id_present: Boolean(first.message_id),
5741
+ deliveryId: context.deliveryId,
5742
+ delivery_correlation_id: context.deliveryId ?? first.message_id
5743
+ };
5744
+ }
5745
+ startRuntimeTrace(agentId, ap, reason, messages) {
4723
5746
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
4724
5747
  const span = this.tracer.startSpan("daemon.runtime.turn", {
4725
5748
  surface: "daemon",
@@ -4729,10 +5752,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4729
5752
  runtime: ap.config.runtime,
4730
5753
  model: ap.config.model,
4731
5754
  reason,
4732
- hasSession: Boolean(ap.sessionId)
5755
+ hasSession: Boolean(ap.sessionId),
5756
+ ...this.messagesTraceAttrs(messages)
4733
5757
  }
4734
5758
  });
4735
- span.addEvent("daemon.turn.started", { reason });
5759
+ span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
4736
5760
  ap.runtimeTraceSpan = span;
4737
5761
  return span;
4738
5762
  }
@@ -4844,6 +5868,48 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4844
5868
  this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
4845
5869
  return true;
4846
5870
  }
5871
+ recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
5872
+ if (ap.inbox.length === 0) return false;
5873
+ if (ap.expectedTerminationReason === "stalled_recovery") {
5874
+ return true;
5875
+ }
5876
+ const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
5877
+ const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
5878
+ const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
5879
+ if (!canRestartStalledProcess) return false;
5880
+ const staleForMs = Date.now() - ap.lastRuntimeEventAt;
5881
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
5882
+ const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
5883
+ ap.runtimeProgressStaleSince ??= Date.now();
5884
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
5885
+ ageMs: staleForMs,
5886
+ staleForMinutes,
5887
+ lastActivity: ap.lastActivity,
5888
+ pendingMessages: ap.inbox.length,
5889
+ recovery: "terminate_for_queued_message"
5890
+ });
5891
+ this.endRuntimeTrace(ap, "error", {
5892
+ outcome: "runtime-stalled",
5893
+ ageMs: staleForMs,
5894
+ lastActivity: ap.lastActivity,
5895
+ pendingMessages: ap.inbox.length,
5896
+ recovery: "terminate_for_queued_message"
5897
+ });
5898
+ ap.expectedTerminationReason = "stalled_recovery";
5899
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
5900
+ logger.warn(
5901
+ `[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
5902
+ );
5903
+ this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
5904
+ try {
5905
+ ap.process.kill("SIGTERM");
5906
+ } catch (err) {
5907
+ const reason = err instanceof Error ? err.message : String(err);
5908
+ logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
5909
+ return false;
5910
+ }
5911
+ return true;
5912
+ }
4847
5913
  /** Handle a single ParsedEvent from any runtime driver */
4848
5914
  handleParsedEvent(agentId, event, driver) {
4849
5915
  const ap = this.agents.get(agentId);
@@ -4967,7 +6033,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4967
6033
  }
4968
6034
  } else {
4969
6035
  ap.isIdle = true;
4970
- this.broadcastActivity(agentId, "online", "Idle");
6036
+ if (ap.lastRuntimeError) {
6037
+ this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
6038
+ } else {
6039
+ this.broadcastActivity(agentId, "online", "Idle");
6040
+ }
4971
6041
  }
4972
6042
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
4973
6043
  if (ap.driver.terminateProcessOnTurnEnd) {
@@ -5006,6 +6076,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5006
6076
  }
5007
6077
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
5008
6078
  this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6079
+ if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6080
+ ap.isIdle = true;
6081
+ ap.pendingNotificationCount = 0;
6082
+ if (ap.notificationTimer) {
6083
+ clearTimeout(ap.notificationTimer);
6084
+ ap.notificationTimer = null;
6085
+ }
6086
+ logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
6087
+ }
5009
6088
  }
5010
6089
  this.broadcastActivity(agentId, "error", event.message, [
5011
6090
  { kind: "text", text: `Error: ${event.message}` }
@@ -5052,20 +6131,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5052
6131
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
5053
6132
  if (encoded) {
5054
6133
  ap.process.stdin?.write(encoded + "\n");
6134
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6135
+ agentId,
6136
+ runtime: ap.config.runtime,
6137
+ model: ap.config.model,
6138
+ launchId: ap.launchId || void 0,
6139
+ outcome: "written",
6140
+ mode: "busy",
6141
+ pending_notification_count: count,
6142
+ session_id_present: true
6143
+ });
6144
+ } else {
6145
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
6146
+ agentId,
6147
+ runtime: ap.config.runtime,
6148
+ model: ap.config.model,
6149
+ launchId: ap.launchId || void 0,
6150
+ outcome: "encode_failed",
6151
+ mode: "busy",
6152
+ pending_notification_count: count,
6153
+ session_id_present: true
6154
+ }, "error");
5055
6155
  }
5056
6156
  }
5057
6157
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
5058
6158
  deliverMessagesViaStdin(agentId, ap, messages, mode) {
5059
6159
  if (messages.length === 0) return true;
6160
+ const split = this.splitRuntimeProfileControlBatch(messages);
6161
+ if (split.deferredMessages.length > 0) {
6162
+ ap.inbox.unshift(...split.deferredMessages);
6163
+ ap.pendingNotificationCount += split.deferredMessages.length;
6164
+ messages = split.nextMessages;
6165
+ this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
6166
+ agentId,
6167
+ launchId: ap.launchId || void 0,
6168
+ runtime: ap.config.runtime,
6169
+ mode,
6170
+ delivered_control_messages_count: messages.length,
6171
+ deferred_messages_count: split.deferredMessages.length,
6172
+ inbox_count: ap.inbox.length,
6173
+ pending_notification_count: ap.pendingNotificationCount
6174
+ });
6175
+ }
6176
+ const traceAttrs = {
6177
+ agentId,
6178
+ launchId: ap.launchId || void 0,
6179
+ runtime: ap.config.runtime,
6180
+ model: ap.config.model,
6181
+ mode,
6182
+ messages_count: messages.length,
6183
+ session_id_present: Boolean(ap.sessionId),
6184
+ inbox_count: ap.inbox.length,
6185
+ pending_notification_count: ap.pendingNotificationCount,
6186
+ busy_delivery_mode: ap.driver.busyDeliveryMode,
6187
+ supports_stdin_notification: ap.driver.supportsStdinNotification,
6188
+ ...this.messagesTraceAttrs(messages)
6189
+ };
5060
6190
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
5061
6191
 
5062
6192
  ${formatIncomingMessage(messages[0], ap.driver)}
5063
6193
 
5064
- Respond as appropriate. Complete all your work before stopping.` : `New messages received:
6194
+ Respond as appropriate. Complete all your work before stopping.
6195
+ ${RESPONSE_TARGET_HINT}` : `New messages received:
5065
6196
 
5066
6197
  ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
5067
6198
 
5068
- Respond as appropriate. Complete all your work before stopping.`);
6199
+ Respond as appropriate. Complete all your work before stopping.
6200
+ ${RESPONSE_TARGET_HINT}`);
5069
6201
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
5070
6202
  if (!encoded) {
5071
6203
  ap.inbox.unshift(...messages);
@@ -5075,14 +6207,27 @@ Respond as appropriate. Complete all your work before stopping.`);
5075
6207
  logger.warn(
5076
6208
  `[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
5077
6209
  );
6210
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6211
+ ...traceAttrs,
6212
+ outcome: "encode_failed",
6213
+ requeued_messages_count: messages.length
6214
+ }, "error");
5078
6215
  return false;
5079
6216
  }
5080
6217
  const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
5081
6218
  logger.info(
5082
6219
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
5083
6220
  );
6221
+ if (this.containsOrdinaryInboxMessage(messages)) {
6222
+ ap.lastRuntimeError = null;
6223
+ }
5084
6224
  ap.process.stdin?.write(encoded + "\n");
5085
6225
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
6226
+ this.recordDaemonTrace("daemon.agent.stdin_delivery", {
6227
+ ...traceAttrs,
6228
+ outcome: "written",
6229
+ stdin_write_attempted: true
6230
+ });
5086
6231
  return true;
5087
6232
  }
5088
6233
  /** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
@@ -5127,6 +6272,16 @@ var systemClock = {
5127
6272
  clearTimeout: (timer) => clearTimeout(timer)
5128
6273
  };
5129
6274
  var INBOUND_WATCHDOG_MS = 7e4;
6275
+ function durationMsBucket(ms) {
6276
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
6277
+ if (ms === 0) return "0";
6278
+ if (ms <= 1e3) return "1s";
6279
+ if (ms <= 1e4) return "1s-10s";
6280
+ if (ms <= 3e4) return "10s-30s";
6281
+ if (ms <= 6e4) return "30s-60s";
6282
+ if (ms <= 12e4) return "60s-120s";
6283
+ return "120s+";
6284
+ }
5130
6285
  var DaemonConnection = class {
5131
6286
  ws = null;
5132
6287
  options;
@@ -5138,6 +6293,8 @@ var DaemonConnection = class {
5138
6293
  shouldConnect = true;
5139
6294
  reconnectAttempt = 0;
5140
6295
  lastDroppedSendLogAt = 0;
6296
+ lastInboundAt = null;
6297
+ lastInboundMessageKind = null;
5141
6298
  constructor(options) {
5142
6299
  this.options = options;
5143
6300
  this.clock = options.clock ?? systemClock;
@@ -5172,6 +6329,10 @@ var DaemonConnection = class {
5172
6329
  this.lastDroppedSendLogAt = now;
5173
6330
  logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
5174
6331
  }
6332
+ this.trace("daemon.connection.outbound_dropped", {
6333
+ message_type: msg.type,
6334
+ ws_ready_state: this.ws?.readyState ?? null
6335
+ });
5175
6336
  }
5176
6337
  get connected() {
5177
6338
  return this.ws?.readyState === WebSocket.OPEN;
@@ -5185,25 +6346,49 @@ var DaemonConnection = class {
5185
6346
  if (wsOptions?.agent) {
5186
6347
  logger.info("[Daemon] Using configured proxy for WebSocket connection");
5187
6348
  }
6349
+ this.trace("daemon.connection.connecting", {
6350
+ reconnect_attempt: this.reconnectAttempt,
6351
+ server_url_present: Boolean(this.options.serverUrl),
6352
+ proxy_present: Boolean(wsOptions?.agent)
6353
+ });
5188
6354
  const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
5189
6355
  this.ws = ws;
5190
6356
  ws.on("open", () => {
5191
6357
  if (this.ws !== ws) return;
5192
6358
  if (!this.shouldConnect) return;
5193
6359
  logger.info("[Daemon] Connected to server");
6360
+ const priorReconnectAttempt = this.reconnectAttempt;
5194
6361
  this.reconnectAttempt = 0;
5195
6362
  this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
6363
+ this.markInbound("websocket_open");
5196
6364
  this.resetWatchdog();
6365
+ this.trace("daemon.connection.connected", {
6366
+ reconnect_attempt: priorReconnectAttempt,
6367
+ inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
6368
+ });
5197
6369
  this.options.onConnect();
5198
6370
  });
5199
6371
  ws.on("message", (data) => {
5200
6372
  if (this.ws !== ws) return;
5201
- this.resetWatchdog();
6373
+ let messageKind = "unknown";
5202
6374
  try {
5203
6375
  const msg = JSON.parse(data.toString());
6376
+ messageKind = msg.type;
6377
+ this.markInbound(messageKind);
6378
+ this.resetWatchdog();
6379
+ this.trace("daemon.connection.inbound_received", {
6380
+ message_type: messageKind,
6381
+ last_inbound_age_ms_bucket: "0"
6382
+ });
5204
6383
  this.options.onMessage(msg);
5205
6384
  } catch (err) {
6385
+ this.markInbound("invalid_json");
6386
+ this.resetWatchdog();
5206
6387
  logger.error("[Daemon] Invalid message from server", err);
6388
+ this.trace("daemon.connection.invalid_message", {
6389
+ error_class: err instanceof Error ? err.name : typeof err,
6390
+ last_inbound_message_kind: "invalid_json"
6391
+ }, "error");
5207
6392
  }
5208
6393
  });
5209
6394
  ws.on("close", (code, reasonBuffer) => {
@@ -5214,12 +6399,23 @@ var DaemonConnection = class {
5214
6399
  logger.warn(
5215
6400
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
5216
6401
  );
6402
+ this.trace("daemon.connection.disconnected", {
6403
+ close_code: code,
6404
+ close_reason_present: Boolean(reason),
6405
+ reconnecting: this.shouldConnect,
6406
+ reconnect_attempt: this.reconnectAttempt,
6407
+ last_inbound_message_kind: this.lastInboundMessageKind,
6408
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
6409
+ }, this.shouldConnect ? "cancelled" : "ok");
5217
6410
  this.options.onDisconnect();
5218
6411
  this.scheduleReconnect();
5219
6412
  });
5220
6413
  ws.on("error", (err) => {
5221
6414
  if (this.ws !== ws) return;
5222
6415
  logger.error(`[Daemon] WebSocket error: ${err.message}`);
6416
+ this.trace("daemon.connection.error", {
6417
+ error_class: err.name || "Error"
6418
+ }, "error");
5223
6419
  });
5224
6420
  }
5225
6421
  scheduleReconnect() {
@@ -5227,6 +6423,10 @@ var DaemonConnection = class {
5227
6423
  if (this.reconnectTimer) return;
5228
6424
  this.reconnectAttempt += 1;
5229
6425
  logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
6426
+ this.trace("daemon.connection.reconnect_scheduled", {
6427
+ reconnect_attempt: this.reconnectAttempt,
6428
+ delay_ms: this.reconnectDelay
6429
+ });
5230
6430
  this.reconnectTimer = this.clock.setTimeout(() => {
5231
6431
  this.reconnectTimer = null;
5232
6432
  this.doConnect();
@@ -5238,6 +6438,13 @@ var DaemonConnection = class {
5238
6438
  const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
5239
6439
  this.watchdogTimer = this.clock.setTimeout(() => {
5240
6440
  logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
6441
+ this.trace("daemon.connection.watchdog_timeout", {
6442
+ inbound_watchdog_ms: ms,
6443
+ last_inbound_message_kind: this.lastInboundMessageKind,
6444
+ last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
6445
+ ws_ready_state: this.ws?.readyState ?? null,
6446
+ reconnecting: this.shouldConnect
6447
+ }, "error");
5241
6448
  try {
5242
6449
  this.ws?.terminate();
5243
6450
  } catch {
@@ -5250,6 +6457,16 @@ var DaemonConnection = class {
5250
6457
  this.watchdogTimer = null;
5251
6458
  }
5252
6459
  }
6460
+ markInbound(messageKind) {
6461
+ this.lastInboundAt = this.clock.now();
6462
+ this.lastInboundMessageKind = messageKind;
6463
+ }
6464
+ lastInboundAgeBucket() {
6465
+ return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
6466
+ }
6467
+ trace(name, attrs, status = "ok") {
6468
+ this.options.onTraceEvent?.(name, attrs, status);
6469
+ }
5253
6470
  };
5254
6471
 
5255
6472
  // src/reminderCache.ts
@@ -5333,10 +6550,10 @@ var ReminderCache = class {
5333
6550
 
5334
6551
  // src/machineLock.ts
5335
6552
  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";
6553
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
6554
+ import os6 from "os";
5338
6555
  import path12 from "path";
5339
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
6556
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
5340
6557
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
5341
6558
  var DaemonMachineLockConflictError = class extends Error {
5342
6559
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -5359,14 +6576,14 @@ function ownerPath(lockDir) {
5359
6576
  }
5360
6577
  function readOwner(lockDir) {
5361
6578
  try {
5362
- return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
6579
+ return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
5363
6580
  } catch {
5364
6581
  return null;
5365
6582
  }
5366
6583
  }
5367
6584
  function lockAgeMs(lockDir) {
5368
6585
  try {
5369
- return Date.now() - statSync2(lockDir).mtimeMs;
6586
+ return Date.now() - statSync3(lockDir).mtimeMs;
5370
6587
  } catch {
5371
6588
  return null;
5372
6589
  }
@@ -5395,7 +6612,7 @@ function acquireDaemonMachineLock(options) {
5395
6612
  const owner = {
5396
6613
  pid: process.pid,
5397
6614
  token,
5398
- hostname: os5.hostname(),
6615
+ hostname: os6.hostname(),
5399
6616
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5400
6617
  serverUrl: options.serverUrl,
5401
6618
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5437,6 +6654,418 @@ function acquireDaemonMachineLock(options) {
5437
6654
  throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
5438
6655
  }
5439
6656
 
6657
+ // src/localTraceSink.ts
6658
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6659
+ import path13 from "path";
6660
+ var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6661
+ var DEFAULT_MAX_FILES = 8;
6662
+ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6663
+ "serverId",
6664
+ "machineId",
6665
+ "agentId",
6666
+ "messageId",
6667
+ "launchId",
6668
+ "uploadId",
6669
+ "bundleId",
6670
+ "deliveryId",
6671
+ "deliveryCorrelationId",
6672
+ "delivery_correlation_id"
6673
+ ]);
6674
+ var LocalRotatingTraceSink = class {
6675
+ traceDir;
6676
+ maxFileBytes;
6677
+ maxFiles;
6678
+ currentFile = null;
6679
+ currentSize = 0;
6680
+ sequence = 0;
6681
+ constructor(options) {
6682
+ this.traceDir = path13.join(options.machineDir, "traces");
6683
+ this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6684
+ this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
6685
+ }
6686
+ record(span) {
6687
+ try {
6688
+ const line = `${JSON.stringify(toLocalTraceRecord(span))}
6689
+ `;
6690
+ this.ensureFile(Buffer.byteLength(line));
6691
+ appendFileSync(this.currentFile, line, { encoding: "utf8" });
6692
+ this.currentSize += Buffer.byteLength(line);
6693
+ } catch {
6694
+ }
6695
+ }
6696
+ getCurrentFile() {
6697
+ return this.currentFile;
6698
+ }
6699
+ ensureFile(nextBytes) {
6700
+ mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
6701
+ if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
6702
+ this.currentFile = path13.join(
6703
+ this.traceDir,
6704
+ `daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6705
+ );
6706
+ writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
6707
+ this.currentSize = statSync4(this.currentFile).size;
6708
+ this.pruneOldFiles();
6709
+ }
6710
+ }
6711
+ pruneOldFiles() {
6712
+ const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
6713
+ const excess = files.length - this.maxFiles;
6714
+ if (excess <= 0) return;
6715
+ for (const file of files.slice(0, excess)) {
6716
+ rmSync3(path13.join(this.traceDir, file), { force: true });
6717
+ }
6718
+ }
6719
+ };
6720
+ function safeTimestamp(timeMs) {
6721
+ return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
6722
+ }
6723
+ function toLocalTraceRecord(span) {
6724
+ return {
6725
+ type: "span",
6726
+ schema_version: 1,
6727
+ trace_id: span.context.traceId,
6728
+ span_id: span.context.spanId,
6729
+ parent_span_id: span.context.parentSpanId,
6730
+ name: span.name,
6731
+ surface: span.surface,
6732
+ kind: span.kind,
6733
+ status: span.status,
6734
+ start_time: new Date(span.startTimeMs).toISOString(),
6735
+ end_time: new Date(span.endTimeMs).toISOString(),
6736
+ duration_ms: span.durationMs,
6737
+ attrs: sanitizeAttrs(span.attrs),
6738
+ events: span.events.map(sanitizeEvent)
6739
+ };
6740
+ }
6741
+ function sanitizeEvent(event) {
6742
+ return {
6743
+ name: event.name,
6744
+ time: new Date(event.timeMs).toISOString(),
6745
+ attrs: sanitizeAttrs(event.attrs)
6746
+ };
6747
+ }
6748
+ function sanitizeAttrs(attrs) {
6749
+ if (!attrs) return void 0;
6750
+ const sanitized = {};
6751
+ for (const [key, value] of Object.entries(attrs)) {
6752
+ if (isDiagnosticIdAttr(key)) {
6753
+ if (value === null || value === void 0 || value === "") continue;
6754
+ sanitized[key] = sanitizeValue(value);
6755
+ continue;
6756
+ }
6757
+ if (shouldDropAttr(key)) continue;
6758
+ sanitized[key] = sanitizeValue(value);
6759
+ }
6760
+ return sanitized;
6761
+ }
6762
+ function sanitizeValue(value) {
6763
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6764
+ return value;
6765
+ }
6766
+ if (Array.isArray(value)) {
6767
+ return { items_count: value.length };
6768
+ }
6769
+ if (typeof value === "object") {
6770
+ return { object_present: true };
6771
+ }
6772
+ return String(value);
6773
+ }
6774
+ function shouldDropAttr(key) {
6775
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
6776
+ if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
6777
+ return true;
6778
+ }
6779
+ if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
6780
+ return false;
6781
+ }
6782
+ if (/(^|_)id$/.test(normalized)) {
6783
+ return true;
6784
+ }
6785
+ return /(^|_)(prompt|content|text|message|body|request|response|command|argv|env|cwd|path|file|error|tool_args|tool_input|tool_output|stdout|stderr)(_|$)/i.test(normalized);
6786
+ }
6787
+ function isDiagnosticIdAttr(key) {
6788
+ return DIAGNOSTIC_ID_ATTRS.has(key);
6789
+ }
6790
+
6791
+ // src/traceBundleUpload.ts
6792
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
6793
+ import { gzipSync } from "zlib";
6794
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
6795
+ import path14 from "path";
6796
+
6797
+ // src/directUploadCapability.ts
6798
+ function joinUrl(base, path16) {
6799
+ return `${base.replace(/\/+$/, "")}${path16}`;
6800
+ }
6801
+ function jsonHeaders(apiKey) {
6802
+ return {
6803
+ "Content-Type": "application/json",
6804
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
6805
+ };
6806
+ }
6807
+ async function requestDaemonScopeAttestation({
6808
+ serverUrl,
6809
+ apiKey,
6810
+ scope,
6811
+ metadata,
6812
+ fetchImpl = fetch,
6813
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6814
+ }) {
6815
+ const { response, data } = await executeJsonRequest(
6816
+ joinUrl(serverUrl, "/internal/machine/scope-attestation"),
6817
+ {
6818
+ method: "POST",
6819
+ headers: jsonHeaders(apiKey),
6820
+ body: JSON.stringify({
6821
+ scope,
6822
+ ...metadata ? { metadata } : {}
6823
+ })
6824
+ },
6825
+ {
6826
+ toolName: "daemon_direct_upload.scope_attestation",
6827
+ target: scope,
6828
+ timeoutMs,
6829
+ fetchImpl
6830
+ }
6831
+ );
6832
+ if (!response.ok) {
6833
+ throw new Error(`Failed to request daemon scope attestation (${response.status})`);
6834
+ }
6835
+ return data;
6836
+ }
6837
+ async function createDirectUploadSession({
6838
+ serverUrl,
6839
+ apiKey,
6840
+ workerUrl,
6841
+ scope,
6842
+ createPath = "/api/uploads",
6843
+ body,
6844
+ attestationMetadata,
6845
+ fetchImpl = fetch,
6846
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6847
+ }) {
6848
+ const capability = await requestDaemonScopeAttestation({
6849
+ serverUrl,
6850
+ apiKey,
6851
+ scope,
6852
+ metadata: attestationMetadata,
6853
+ fetchImpl,
6854
+ timeoutMs
6855
+ });
6856
+ const { response, data } = await executeJsonRequest(
6857
+ joinUrl(workerUrl, createPath),
6858
+ {
6859
+ method: "POST",
6860
+ headers: jsonHeaders(),
6861
+ body: JSON.stringify({
6862
+ ...body,
6863
+ attestation: capability.attestation
6864
+ })
6865
+ },
6866
+ {
6867
+ toolName: "daemon_direct_upload.create",
6868
+ target: capability.audience,
6869
+ timeoutMs,
6870
+ fetchImpl
6871
+ }
6872
+ );
6873
+ if (!response.ok) {
6874
+ throw new Error(`Failed to create direct upload session (${response.status})`);
6875
+ }
6876
+ return { capability, response: data };
6877
+ }
6878
+ async function uploadWithSignedCapability({
6879
+ serverUrl,
6880
+ apiKey,
6881
+ workerUrl,
6882
+ scope,
6883
+ createPath = "/api/uploads",
6884
+ createBody,
6885
+ attestationMetadata,
6886
+ uploadBody,
6887
+ fetchImpl = fetch,
6888
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
6889
+ }) {
6890
+ const { capability, response: session } = await createDirectUploadSession({
6891
+ serverUrl,
6892
+ apiKey,
6893
+ workerUrl,
6894
+ scope,
6895
+ createPath,
6896
+ body: createBody,
6897
+ attestationMetadata,
6898
+ fetchImpl,
6899
+ timeoutMs
6900
+ });
6901
+ const { response: uploadResponse } = await executeResponseRequest(
6902
+ session.upload.url,
6903
+ {
6904
+ method: session.upload.method,
6905
+ headers: session.upload.headers ?? {},
6906
+ body: uploadBody
6907
+ },
6908
+ {
6909
+ toolName: "daemon_direct_upload.put",
6910
+ target: capability.audience,
6911
+ timeoutMs,
6912
+ fetchImpl
6913
+ }
6914
+ );
6915
+ if (!uploadResponse.ok) {
6916
+ throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
6917
+ }
6918
+ return { capability, session, uploadResponse };
6919
+ }
6920
+
6921
+ // src/traceBundleUpload.ts
6922
+ var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
6923
+ var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
6924
+ var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
6925
+ var DEFAULT_MAX_FILES_PER_RUN = 4;
6926
+ var DaemonTraceBundleUploader = class {
6927
+ options;
6928
+ timer = null;
6929
+ constructor(options) {
6930
+ this.options = options;
6931
+ }
6932
+ start() {
6933
+ if (this.timer) return;
6934
+ void this.uploadOnce();
6935
+ this.timer = setInterval(() => {
6936
+ void this.uploadOnce();
6937
+ }, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
6938
+ }
6939
+ stop() {
6940
+ if (!this.timer) return;
6941
+ clearInterval(this.timer);
6942
+ this.timer = null;
6943
+ }
6944
+ async uploadOnce() {
6945
+ const files = await this.findUploadCandidates();
6946
+ let uploaded = 0;
6947
+ for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
6948
+ if (await this.uploadFile(file)) uploaded += 1;
6949
+ }
6950
+ return { attempted: files.length, uploaded };
6951
+ }
6952
+ async findUploadCandidates() {
6953
+ const traceDir = path14.join(this.options.machineDir, "traces");
6954
+ let names;
6955
+ try {
6956
+ names = await readdir3(traceDir);
6957
+ } catch {
6958
+ return [];
6959
+ }
6960
+ const now = Date.now();
6961
+ const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
6962
+ const currentFile = this.options.currentFileProvider?.();
6963
+ const candidates = [];
6964
+ for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
6965
+ const file = path14.join(traceDir, name);
6966
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
6967
+ if (await this.isUploaded(file)) continue;
6968
+ try {
6969
+ const info = await stat3(file);
6970
+ if (!info.isFile() || info.size <= 0) continue;
6971
+ if (now - info.mtimeMs < minAgeMs) continue;
6972
+ candidates.push(file);
6973
+ } catch {
6974
+ }
6975
+ }
6976
+ return candidates;
6977
+ }
6978
+ async uploadFile(file) {
6979
+ const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
6980
+ surface: "daemon",
6981
+ kind: "producer",
6982
+ attrs: {
6983
+ file_present: true,
6984
+ worker_url_present: Boolean(this.options.workerUrl)
6985
+ }
6986
+ });
6987
+ try {
6988
+ const raw = await readFile2(file);
6989
+ if (raw.byteLength === 0) {
6990
+ span?.end("cancelled", { attrs: { outcome: "empty" } });
6991
+ return false;
6992
+ }
6993
+ const gzipped = gzipSync(raw);
6994
+ const bundleSha256 = sha256Hex(gzipped);
6995
+ const bundleId = randomUUID3();
6996
+ await uploadWithSignedCapability({
6997
+ serverUrl: this.options.serverUrl,
6998
+ apiKey: this.options.apiKey,
6999
+ workerUrl: this.options.workerUrl,
7000
+ scope: TRACE_UPLOAD_SCOPE,
7001
+ createPath: "/api/trace-bundles",
7002
+ attestationMetadata: {
7003
+ bundleId,
7004
+ bundleSha256,
7005
+ bundleSizeBytes: gzipped.byteLength
7006
+ },
7007
+ createBody: {
7008
+ bundleSha256,
7009
+ bundleSizeBytes: gzipped.byteLength
7010
+ },
7011
+ uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
7012
+ fetchImpl: this.options.fetchImpl
7013
+ });
7014
+ await this.markUploaded(file, {
7015
+ bundleId,
7016
+ bundleSha256,
7017
+ bundleSizeBytes: gzipped.byteLength
7018
+ });
7019
+ span?.end("ok", {
7020
+ attrs: {
7021
+ bundleId,
7022
+ bundle_size_bytes: gzipped.byteLength
7023
+ }
7024
+ });
7025
+ return true;
7026
+ } catch (err) {
7027
+ span?.end("error", {
7028
+ attrs: {
7029
+ error_class: err instanceof Error ? err.name : "Error",
7030
+ error_message_present: err instanceof Error && Boolean(err.message)
7031
+ }
7032
+ });
7033
+ return false;
7034
+ }
7035
+ }
7036
+ uploadStatePath(file) {
7037
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7038
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
7039
+ }
7040
+ async isUploaded(file) {
7041
+ try {
7042
+ await stat3(this.uploadStatePath(file));
7043
+ return true;
7044
+ } catch {
7045
+ return false;
7046
+ }
7047
+ }
7048
+ async markUploaded(file, metadata) {
7049
+ const stateFile = this.uploadStatePath(file);
7050
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
7051
+ await writeFile2(stateFile, `${JSON.stringify({
7052
+ file: path14.basename(file),
7053
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7054
+ ...metadata
7055
+ }, null, 2)}
7056
+ `, { mode: 384 });
7057
+ }
7058
+ };
7059
+ function sha256Hex(body) {
7060
+ return createHash2("sha256").update(body).digest("hex");
7061
+ }
7062
+ function readPositiveIntegerEnv2(name, fallback) {
7063
+ const value = process.env[name];
7064
+ if (!value) return fallback;
7065
+ const parsed = Number(value);
7066
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
7067
+ }
7068
+
5440
7069
  // src/core.ts
5441
7070
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
5442
7071
  function parseDaemonCliArgs(args) {
@@ -5458,59 +7087,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5458
7087
  }
5459
7088
  }
5460
7089
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5461
- const dirname = path13.dirname(fileURLToPath(moduleUrl));
5462
- const jsPath = path13.resolve(dirname, "chat-bridge.js");
7090
+ const dirname = path15.dirname(fileURLToPath(moduleUrl));
7091
+ const jsPath = path15.resolve(dirname, "chat-bridge.js");
5463
7092
  try {
5464
7093
  accessSync(jsPath);
5465
7094
  return jsPath;
5466
7095
  } catch {
5467
- return path13.resolve(dirname, "chat-bridge.ts");
7096
+ return path15.resolve(dirname, "chat-bridge.ts");
5468
7097
  }
5469
7098
  }
5470
7099
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5471
- const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5472
- const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
7100
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7101
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
5473
7102
  try {
5474
7103
  accessSync(bundledDistPath);
5475
7104
  return bundledDistPath;
5476
7105
  } catch {
5477
- const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7106
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5478
7107
  accessSync(workspaceDistPath);
5479
7108
  return workspaceDistPath;
5480
7109
  }
5481
7110
  }
5482
- function detectRuntimes() {
7111
+ function detectRuntimes(tracer = noopTracer) {
5483
7112
  const ids = [];
5484
7113
  const versions = {};
7114
+ const span = tracer.startSpan("daemon.runtime.detect", {
7115
+ surface: "daemon",
7116
+ kind: "internal",
7117
+ attrs: {
7118
+ known_runtime_count: RUNTIMES.length
7119
+ }
7120
+ });
5485
7121
  for (const runtime of RUNTIMES) {
5486
7122
  const driver = getDriver(runtime.id);
7123
+ let probeErrorPresent = false;
5487
7124
  try {
5488
7125
  if (driver.probe) {
5489
7126
  const probe = driver.probe();
5490
7127
  if (!probe.available) {
5491
7128
  if (probe.version) versions[runtime.id] = probe.version;
7129
+ span.addEvent("daemon.runtime.detect.checked", {
7130
+ runtime: runtime.id,
7131
+ outcome: "unavailable",
7132
+ version_present: Boolean(probe.version),
7133
+ binary_path_present: false
7134
+ });
5492
7135
  continue;
5493
7136
  }
5494
7137
  ids.push(runtime.id);
5495
7138
  if (probe.version) versions[runtime.id] = probe.version;
7139
+ span.addEvent("daemon.runtime.detect.checked", {
7140
+ runtime: runtime.id,
7141
+ outcome: "available",
7142
+ version_present: Boolean(probe.version),
7143
+ binary_path_present: false
7144
+ });
5496
7145
  continue;
5497
7146
  }
5498
7147
  } catch {
7148
+ probeErrorPresent = true;
5499
7149
  }
5500
7150
  const detectionBinaries = [runtime.binary];
7151
+ let detectedByPath = false;
5501
7152
  for (const binary of detectionBinaries) {
5502
7153
  const resolved = resolveCommandOnPath(binary);
5503
7154
  if (!resolved) continue;
5504
7155
  ids.push(runtime.id);
7156
+ detectedByPath = true;
5505
7157
  const version = readCommandVersion(binary);
5506
7158
  if (version) {
5507
7159
  versions[runtime.id] = version;
5508
7160
  }
7161
+ span.addEvent("daemon.runtime.detect.checked", {
7162
+ runtime: runtime.id,
7163
+ outcome: "available",
7164
+ version_present: Boolean(version),
7165
+ binary_path_present: true,
7166
+ probe_error_present: probeErrorPresent
7167
+ });
5509
7168
  break;
5510
7169
  }
7170
+ if (!detectedByPath) {
7171
+ span.addEvent("daemon.runtime.detect.checked", {
7172
+ runtime: runtime.id,
7173
+ outcome: "unavailable",
7174
+ version_present: false,
7175
+ binary_path_present: false,
7176
+ probe_error_present: probeErrorPresent
7177
+ });
7178
+ }
5511
7179
  }
7180
+ span.end("ok", {
7181
+ attrs: {
7182
+ detected_runtime_count: ids.length
7183
+ }
7184
+ });
5512
7185
  return { ids, versions };
5513
7186
  }
7187
+ function readPositiveIntegerEnv3(name, fallback) {
7188
+ const raw = process.env[name];
7189
+ if (!raw) return fallback;
7190
+ const parsed = Number(raw);
7191
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
7192
+ return Math.floor(parsed);
7193
+ }
5514
7194
  function formatChannelTarget(msg) {
5515
7195
  return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
5516
7196
  }
@@ -5534,6 +7214,8 @@ function summarizeIncomingMessage(msg) {
5534
7214
  return `(agent=${msg.agentId}, path=${msg.path})`;
5535
7215
  case "agent:skills:list":
5536
7216
  return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
7217
+ case "agent:activity_probe":
7218
+ return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
5537
7219
  case "machine:workspace:delete":
5538
7220
  return `(directory=${msg.directoryName})`;
5539
7221
  case "machine:runtime_models:detect":
@@ -5558,14 +7240,18 @@ var DaemonCore = class {
5558
7240
  connection;
5559
7241
  reminderCache;
5560
7242
  tracer;
7243
+ injectedTracer;
5561
7244
  machineLock = null;
7245
+ localTraceSink = null;
7246
+ traceBundleUploader = null;
5562
7247
  constructor(options) {
5563
7248
  this.options = options;
5564
7249
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
5565
7250
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
5566
7251
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
5567
- this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
7252
+ this.injectedTracer = Boolean(options.tracer);
5568
7253
  this.tracer = options.tracer ?? noopTracer;
7254
+ this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
5569
7255
  this.reminderCache = new ReminderCache({
5570
7256
  clock: options.reminderClock,
5571
7257
  onFire: (job) => this.onReminderFire(job)
@@ -5575,7 +7261,8 @@ var DaemonCore = class {
5575
7261
  dataDir: options.dataDir,
5576
7262
  serverUrl: options.serverUrl,
5577
7263
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
5578
- slockCliPath: this.slockCliPath
7264
+ slockCliPath: this.slockCliPath,
7265
+ tracer: this.tracer
5579
7266
  };
5580
7267
  this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
5581
7268
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -5585,15 +7272,48 @@ var DaemonCore = class {
5585
7272
  ...options.connectionOptions,
5586
7273
  onMessage: (msg) => this.handleMessage(msg),
5587
7274
  onConnect: () => this.handleConnect(),
5588
- onDisconnect: () => this.handleDisconnect()
7275
+ onDisconnect: () => this.handleDisconnect(),
7276
+ onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
5589
7277
  });
5590
7278
  this.connection = connection;
5591
7279
  }
5592
7280
  resolveMachineStateRoot() {
5593
7281
  if (this.options.machineStateDir) return this.options.machineStateDir;
5594
- if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
7282
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
5595
7283
  return DEFAULT_MACHINE_STATE_ROOT;
5596
7284
  }
7285
+ shouldEnableLocalTrace() {
7286
+ if (this.injectedTracer) return false;
7287
+ if (!this.options.localTrace) return false;
7288
+ return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
7289
+ }
7290
+ installLocalTraceSink(machineDir) {
7291
+ if (!this.shouldEnableLocalTrace()) return;
7292
+ this.localTraceSink = new LocalRotatingTraceSink({
7293
+ machineDir,
7294
+ maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
7295
+ maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
7296
+ });
7297
+ this.tracer = new BasicTracer({
7298
+ sink: this.localTraceSink
7299
+ });
7300
+ this.agentManager.setTracer(this.tracer);
7301
+ }
7302
+ installTraceBundleUploader(machineDir) {
7303
+ if (!this.shouldEnableLocalTrace()) return;
7304
+ if (this.traceBundleUploader) return;
7305
+ const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
7306
+ if (!workerUrl) return;
7307
+ this.traceBundleUploader = new DaemonTraceBundleUploader({
7308
+ machineDir,
7309
+ serverUrl: this.options.serverUrl,
7310
+ apiKey: this.options.apiKey,
7311
+ workerUrl,
7312
+ tracer: this.tracer,
7313
+ currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
7314
+ });
7315
+ this.traceBundleUploader.start();
7316
+ }
5597
7317
  start() {
5598
7318
  logger.info("[Slock Daemon] Starting...");
5599
7319
  if (!this.machineLock) {
@@ -5603,10 +7323,24 @@ var DaemonCore = class {
5603
7323
  rootDir: this.resolveMachineStateRoot()
5604
7324
  });
5605
7325
  logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
7326
+ this.installLocalTraceSink(this.machineLock.machineDir);
7327
+ this.installTraceBundleUploader(this.machineLock.machineDir);
7328
+ const span = this.tracer.startSpan("daemon.lifecycle.start", {
7329
+ surface: "daemon",
7330
+ kind: "internal",
7331
+ attrs: {
7332
+ machine_dir_present: true,
7333
+ local_trace_enabled: this.shouldEnableLocalTrace()
7334
+ }
7335
+ });
7336
+ span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
7337
+ span.end("ok");
5606
7338
  }
5607
7339
  try {
5608
7340
  this.connection.connect();
5609
7341
  } catch (err) {
7342
+ this.traceBundleUploader?.stop();
7343
+ this.traceBundleUploader = null;
5610
7344
  this.machineLock.release();
5611
7345
  this.machineLock = null;
5612
7346
  throw err;
@@ -5614,13 +7348,24 @@ var DaemonCore = class {
5614
7348
  }
5615
7349
  async stop() {
5616
7350
  logger.info("[Slock Daemon] Shutting down...");
7351
+ const span = this.tracer.startSpan("daemon.lifecycle.stop", {
7352
+ surface: "daemon",
7353
+ kind: "internal",
7354
+ attrs: { machine_lock_present: Boolean(this.machineLock) }
7355
+ });
5617
7356
  this.reminderCache.clear();
7357
+ this.traceBundleUploader?.stop();
7358
+ this.traceBundleUploader = null;
5618
7359
  try {
5619
7360
  await this.agentManager.stopAll();
7361
+ span.addEvent("daemon.agents.stopped");
5620
7362
  } finally {
5621
7363
  this.connection.disconnect();
7364
+ span.addEvent("daemon.connection.disconnect_requested");
5622
7365
  this.machineLock?.release();
7366
+ if (this.machineLock) span.addEvent("daemon.machine_lock.released");
5623
7367
  this.machineLock = null;
7368
+ span.end("ok");
5624
7369
  }
5625
7370
  }
5626
7371
  get connected() {
@@ -5629,6 +7374,14 @@ var DaemonCore = class {
5629
7374
  getRunningAgentIds() {
5630
7375
  return this.agentManager.getRunningAgentIds();
5631
7376
  }
7377
+ recordDaemonTrace(name, attrs, status = "ok") {
7378
+ const span = this.tracer.startSpan(name, {
7379
+ surface: "daemon",
7380
+ kind: "internal",
7381
+ attrs
7382
+ });
7383
+ span.end(status);
7384
+ }
5632
7385
  handleMessage(msg) {
5633
7386
  const summary = summarizeIncomingMessage(msg);
5634
7387
  logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
@@ -5658,50 +7411,88 @@ var DaemonCore = class {
5658
7411
  kind: "consumer",
5659
7412
  attrs: {
5660
7413
  agentId: msg.agentId,
7414
+ deliveryId: msg.deliveryId,
7415
+ delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
7416
+ messageId: msg.message.message_id,
7417
+ message_id_present: Boolean(msg.message.message_id),
5661
7418
  seq: msg.seq
5662
7419
  }
5663
7420
  });
5664
7421
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
5665
7422
  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");
7423
+ span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
7424
+ const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
7425
+ span.addEvent("daemon.deliver_to_agent_manager", { accepted });
7426
+ if (!accepted) {
7427
+ span.end("ok", { attrs: { outcome: "not-accepted" } });
7428
+ break;
7429
+ }
5669
7430
  const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
5670
7431
  span.addEvent("daemon.ack.sent", { seq: ackSeq });
5671
7432
  this.connection.send({
5672
7433
  type: "agent:deliver:ack",
5673
7434
  agentId: msg.agentId,
5674
7435
  seq: ackSeq,
5675
- traceparent: formatTraceparent(span.context)
7436
+ traceparent: formatTraceparent(span.context),
7437
+ deliveryId: msg.deliveryId
5676
7438
  });
5677
- span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
7439
+ span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
5678
7440
  } catch (err) {
5679
- span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
7441
+ span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
5680
7442
  throw err;
5681
7443
  }
5682
7444
  break;
5683
7445
  }
5684
- case "agent:runtime_profile:migration":
7446
+ case "agent:runtime_profile:migration": {
7447
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7448
+ parent: parseTraceparent(msg.traceparent),
7449
+ surface: "daemon",
7450
+ kind: "consumer",
7451
+ attrs: {
7452
+ agentId: msg.agentId,
7453
+ control_kind: "migration",
7454
+ key_present: Boolean(msg.migrationKey),
7455
+ launchId: msg.launchId || void 0
7456
+ }
7457
+ });
5685
7458
  logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
5686
- this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
7459
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
7460
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
5687
7461
  break;
5688
- case "agent:runtime_profile:daemon_release_notice":
7462
+ }
7463
+ case "agent:runtime_profile:daemon_release_notice": {
7464
+ const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
7465
+ parent: parseTraceparent(msg.traceparent),
7466
+ surface: "daemon",
7467
+ kind: "consumer",
7468
+ attrs: {
7469
+ agentId: msg.agentId,
7470
+ control_kind: "daemon_release_notice",
7471
+ key_present: Boolean(msg.noticeKey),
7472
+ launchId: msg.launchId || void 0
7473
+ }
7474
+ });
5689
7475
  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);
7476
+ const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
7477
+ span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
5691
7478
  break;
7479
+ }
5692
7480
  case "agent:workspace:list":
5693
7481
  this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
5694
7482
  this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
5695
7483
  });
5696
7484
  break;
5697
7485
  case "agent:workspace:read":
5698
- this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
7486
+ this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
5699
7487
  this.connection.send({
5700
7488
  type: "agent:workspace:file_content",
5701
7489
  agentId: msg.agentId,
5702
7490
  requestId: msg.requestId,
5703
7491
  content,
5704
- binary
7492
+ binary,
7493
+ size,
7494
+ mimeType,
7495
+ encoding
5705
7496
  });
5706
7497
  }).catch(() => {
5707
7498
  this.connection.send({
@@ -5709,7 +7500,8 @@ var DaemonCore = class {
5709
7500
  agentId: msg.agentId,
5710
7501
  requestId: msg.requestId,
5711
7502
  content: null,
5712
- binary: false
7503
+ binary: false,
7504
+ size: 0
5713
7505
  });
5714
7506
  });
5715
7507
  break;
@@ -5721,6 +7513,9 @@ var DaemonCore = class {
5721
7513
  this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
5722
7514
  });
5723
7515
  break;
7516
+ case "agent:activity_probe":
7517
+ this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
7518
+ break;
5724
7519
  case "machine:workspace:scan":
5725
7520
  logger.info("[Daemon] Scanning all workspace directories");
5726
7521
  this.agentManager.scanAllWorkspaces().then((directories) => {
@@ -5738,7 +7533,12 @@ var DaemonCore = class {
5738
7533
  const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
5739
7534
  Promise.resolve(detect).then((result) => {
5740
7535
  if (result) {
5741
- this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models: result.models, default: result.default });
7536
+ const verified = driver.model.detectedModelsVerifiedAs;
7537
+ const models = result.models.map((model) => ({
7538
+ ...model,
7539
+ verified: model.verified ?? verified
7540
+ }));
7541
+ this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
5742
7542
  } else {
5743
7543
  this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
5744
7544
  }
@@ -5777,35 +7577,59 @@ var DaemonCore = class {
5777
7577
  const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
5778
7578
  const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
5779
7579
  logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
7580
+ const runningAgentIds = this.agentManager.getRunningAgentIds();
7581
+ const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
7582
+ const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
5780
7583
  this.connection.send({
5781
7584
  type: "ready",
5782
7585
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
5783
7586
  runtimes,
5784
- runningAgents: this.agentManager.getRunningAgentIds(),
5785
- hostname: this.options.hostname ?? os6.hostname(),
5786
- os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
7587
+ runningAgents: runningAgentIds,
7588
+ hostname: this.options.hostname ?? os7.hostname(),
7589
+ os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
5787
7590
  daemonVersion: this.daemonVersion
5788
7591
  });
5789
- for (const agentId of this.agentManager.getRunningAgentIds()) {
7592
+ this.recordDaemonTrace("daemon.ready.sent", {
7593
+ runtimes_count: runtimes.length,
7594
+ running_agents_count: runningAgentIds.length,
7595
+ idle_agents_count: idleAgentSessions.length,
7596
+ runtime_profile_reports_count: runtimeProfileReports.length,
7597
+ daemon_version_present: Boolean(this.daemonVersion)
7598
+ });
7599
+ for (const agentId of runningAgentIds) {
5790
7600
  const sessionId = this.agentManager.getAgentSessionId(agentId);
5791
7601
  const launchId = this.agentManager.getAgentLaunchId(agentId);
5792
7602
  if (sessionId) {
5793
7603
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
5794
7604
  }
5795
7605
  }
5796
- for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
7606
+ for (const { agentId, sessionId, launchId } of idleAgentSessions) {
5797
7607
  this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
5798
7608
  }
5799
- for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
7609
+ for (const report of runtimeProfileReports) {
7610
+ const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
7611
+ surface: "daemon",
7612
+ kind: "producer",
7613
+ attrs: {
7614
+ agentId: report.agentId,
7615
+ launchId: report.launchId || void 0,
7616
+ runtime: report.facts.runtime,
7617
+ model_present: Boolean(report.facts.model),
7618
+ session_ref_present: Boolean(report.facts.sessionRef),
7619
+ workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
7620
+ }
7621
+ });
5800
7622
  this.connection.send({
5801
7623
  type: "agent:runtime_profile",
5802
7624
  agentId: report.agentId,
5803
7625
  facts: report.facts,
5804
- launchId: report.launchId || void 0
7626
+ launchId: report.launchId || void 0,
7627
+ traceparent: formatTraceparent(span.context)
5805
7628
  });
7629
+ span.end("ok");
5806
7630
  }
5807
- const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
5808
- for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
7631
+ const agentsForSnapshot = new Set(runningAgentIds);
7632
+ for (const { agentId } of idleAgentSessions) {
5809
7633
  agentsForSnapshot.add(agentId);
5810
7634
  }
5811
7635
  for (const agentId of agentsForSnapshot) {
@@ -5815,6 +7639,10 @@ var DaemonCore = class {
5815
7639
  }
5816
7640
  handleDisconnect() {
5817
7641
  logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
7642
+ this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
7643
+ running_agents_count: this.agentManager.getRunningAgentIds().length,
7644
+ idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
7645
+ }, "cancelled");
5818
7646
  this.options.lifecycleHooks?.onDisconnect?.();
5819
7647
  }
5820
7648
  };