@slock-ai/daemon 0.46.2 → 0.47.0-play.20260512173159

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,17 +1,21 @@
1
1
  import {
2
2
  DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
3
+ SLOCK_HOME_ENV,
3
4
  buildWebSocketOptions,
4
5
  executeJsonRequest,
5
6
  executeResponseRequest,
6
- logger
7
- } from "./chunk-Z3PCMYZO.js";
7
+ listLegacySlockStatePaths,
8
+ logger,
9
+ resolveSlockHome,
10
+ resolveSlockHomePath
11
+ } from "./chunk-B7XIMLOT.js";
8
12
 
9
13
  // src/core.ts
10
- import path15 from "path";
14
+ import path16 from "path";
11
15
  import os7 from "os";
12
16
  import { createRequire } from "module";
13
17
  import { accessSync } from "fs";
14
- import { fileURLToPath } from "url";
18
+ import { fileURLToPath as fileURLToPath2 } from "url";
15
19
 
16
20
  // ../shared/src/tracing/index.ts
17
21
  var DEFAULT_TRACE_FLAGS = "00";
@@ -576,6 +580,82 @@ function summarizeToolInput(toolName, input) {
576
580
  // ../shared/src/attachmentPreview.ts
577
581
  var CSV_PREVIEW_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
578
582
 
583
+ // ../shared/src/actionCards.ts
584
+ import { z } from "zod";
585
+ var uuidSchema = z.uuid();
586
+ var idOrHandleSchema = z.string().min(1).max(120);
587
+ var draftHintSchema = z.string().trim().max(2e3).optional().describe(
588
+ "Why the agent prepared this for you. Shows below the form on the card; not the action itself."
589
+ );
590
+ var channelCreateOperationSchema = z.object({
591
+ type: z.literal("channel:create"),
592
+ name: z.string().trim().min(1).max(80),
593
+ visibility: z.enum(["public", "private"]).default("public"),
594
+ description: z.string().trim().max(500).optional(),
595
+ /**
596
+ * Humans to add to the channel on creation. Each entry is a handle
597
+ * (`@alice` or bare `alice`) or a UUID. Server resolves via
598
+ * `resolveUserByName` at prepare time; resolved UUIDs are stored.
599
+ */
600
+ initialHumans: z.array(idOrHandleSchema).max(64).optional(),
601
+ /**
602
+ * Agents to add to the channel on creation. Each entry is a handle
603
+ * (`@scout` or bare `scout`) or a UUID. Server resolves via
604
+ * `resolveAgentByName` at prepare time; resolved UUIDs are stored.
605
+ */
606
+ initialAgents: z.array(idOrHandleSchema).max(64).optional(),
607
+ draftHint: draftHintSchema
608
+ });
609
+ var agentCreateOperationSchema = z.object({
610
+ type: z.literal("agent:create"),
611
+ name: z.string().trim().min(1).max(60),
612
+ description: z.string().trim().max(500).optional(),
613
+ /**
614
+ * Agent can only suggest semantic intent (name + description). Technical
615
+ * configuration (which computer / runtime / model / reasoning effort) is
616
+ * a user prerogative — the human picks those in the create dialog when
617
+ * they click "Create Agent" on the card. Per stdrc 2026-05-10
618
+ * #proj-approval msg=ae4ecedd: "为什么 computer、runtime 和 model 还是
619
+ * 帮用户选了?" — don't let the agent prefill those.
620
+ */
621
+ draftHint: draftHintSchema
622
+ });
623
+ var channelAddMemberOperationSchema = z.object({
624
+ type: z.literal("channel:add_member"),
625
+ /**
626
+ * Target channel. Handle (`#general` or bare `general`) or UUID.
627
+ * Server resolves via `resolveChannelByName` at prepare time; the
628
+ * resolved UUID is stored in the card metadata.
629
+ */
630
+ channel: idOrHandleSchema,
631
+ /** Same resolution rule as `channelCreateOperationSchema.initialHumans`. */
632
+ humans: z.array(idOrHandleSchema).max(64).optional(),
633
+ /** Same resolution rule as `channelCreateOperationSchema.initialAgents`. */
634
+ agents: z.array(idOrHandleSchema).max(64).optional(),
635
+ draftHint: draftHintSchema
636
+ });
637
+ var actionCardActionSchema = z.discriminatedUnion("type", [
638
+ channelCreateOperationSchema,
639
+ agentCreateOperationSchema,
640
+ channelAddMemberOperationSchema
641
+ ]);
642
+
643
+ // ../shared/src/translationLanguages.ts
644
+ var SUPPORTED_TRANSLATION_LANGUAGE_CODES = [
645
+ "en",
646
+ "zh-cn",
647
+ "zh-tw",
648
+ "ja",
649
+ "ko",
650
+ "es",
651
+ "fr",
652
+ "de",
653
+ "pt-br"
654
+ ];
655
+ var SUPPORTED_TRANSLATION_LANGUAGE_SET = new Set(
656
+ SUPPORTED_TRANSLATION_LANGUAGE_CODES
657
+ );
658
+
579
659
  // ../shared/src/testing/failpoints.ts
580
660
  var NoopFailpointRegistry = class {
581
661
  get enabled() {
@@ -645,6 +725,7 @@ var SERVER_CAPABILITY_MATRIX = {
645
725
  var RUNTIMES = [
646
726
  { id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
647
727
  { id: "codex", displayName: "Codex CLI", binary: "codex", supported: true },
728
+ { id: "pi", displayName: "Pi", binary: "pi", supported: true },
648
729
  { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
649
730
  { id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
650
731
  { id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
@@ -686,9 +767,10 @@ var DISPLAY_PLAN_CONFIG = {
686
767
  };
687
768
 
688
769
  // src/agentProcessManager.ts
689
- import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
770
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
690
771
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
691
- import path11 from "path";
772
+ import { createHash as createHash2 } from "crypto";
773
+ import path12 from "path";
692
774
  import os5 from "os";
693
775
 
694
776
  // src/drivers/claude.ts
@@ -781,21 +863,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
781
863
  6. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
782
864
  7. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
783
865
  8. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
784
- 9. **\`slock task list\`** \u2014 View a channel's task board.
785
- 10. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
786
- 11. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
787
- 12. **\`slock task unclaim\`** \u2014 Release your claim on a task.
788
- 13. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
789
- 14. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Uses content sniffing for image previews; pass \`--mime-type\` only when you know the exact type. Returns an attachment ID to pass to \`slock message send\`.
790
- 15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
791
- 16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
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.
793
- 18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
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.
866
+ 9. **\`slock message react\`** \u2014 Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like \u{1F440}, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
867
+ 10. **\`slock task list\`** \u2014 View a channel's task board.
868
+ 11. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
869
+ 12. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
870
+ 13. **\`slock task unclaim\`** \u2014 Release your claim on a task.
871
+ 14. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
872
+ 15. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Uses content sniffing for image previews; pass \`--mime-type\` only when you know the exact type. Returns an attachment ID to pass to \`slock message send\`.
873
+ 16. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
874
+ 17. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
875
+ 18. **\`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.
876
+ 19. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
877
+ 20. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
878
+ 21. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
879
+ 22. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
880
+ 23. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
881
+ 24. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
882
+ 25. **\`slock action prepare\`** \u2014 Prepare an action card for a human to commit (B-mode quick-commit shortcut). Posts a card the human can click to execute the action under their own identity. Pass \`--target <ch>\` and pipe the action JSON on stdin (variants: \`channel:create\`, \`agent:create\`).
799
883
 
800
884
  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:
801
885
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -1093,6 +1177,17 @@ Keep the user informed. They cannot see your internal reasoning, so:
1093
1177
  - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1094
1178
  - When done, summarize the result.
1095
1179
  - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
1180
+ - For long answers where users need the conclusion first but details still matter, put the conclusion and next action outside any collapse, then use sanitized HTML details blocks for optional depth:
1181
+
1182
+ \`\`\`html
1183
+ <details>
1184
+ <summary>Evidence, logs, or edge cases</summary>
1185
+
1186
+ Detailed notes go here.
1187
+ </details>
1188
+ \`\`\`
1189
+
1190
+ Do not hide the main recommendation, blocker, or required action inside \`<details>\`; only fold supporting evidence, logs, alternatives, or extended rationale.
1096
1191
 
1097
1192
  ### Conversation etiquette
1098
1193
 
@@ -1272,6 +1367,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1272
1367
  ...ctx.config.envVars || {},
1273
1368
  ...extraEnv,
1274
1369
  ...runtimeContextEnv(ctx.config),
1370
+ [SLOCK_HOME_ENV]: resolveSlockHome(),
1275
1371
  SLOCK_AGENT_ID: ctx.agentId,
1276
1372
  ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
1277
1373
  SLOCK_SERVER_URL: ctx.config.serverUrl,
@@ -1705,7 +1801,7 @@ var ClaudeDriver = class {
1705
1801
 
1706
1802
  // src/drivers/codex.ts
1707
1803
  import { spawn as spawn2, execSync } from "child_process";
1708
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1804
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1709
1805
  import os2 from "os";
1710
1806
  import path4 from "path";
1711
1807
  function getCodexNotificationErrorMessage(params) {
@@ -1734,11 +1830,29 @@ function ensureGitRepoForCodex(workingDirectory, deps = {}) {
1734
1830
  );
1735
1831
  }
1736
1832
  var CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
1833
+ function resolveWindowsSandboxRunner(deps = {}) {
1834
+ const homeDir = deps.homeDir ?? os2.homedir();
1835
+ const sandboxBinDir = path4.join(homeDir, ".codex", ".sandbox-bin");
1836
+ if (!(deps.existsSyncFn ?? existsSync3)(sandboxBinDir)) return null;
1837
+ try {
1838
+ const files = readdirSync2(sandboxBinDir);
1839
+ const runners = files.filter((f) => f.startsWith("codex-command-runner") && f.endsWith(".exe")).sort((a, b) => b.localeCompare(a));
1840
+ if (runners.length > 0) return path4.join(sandboxBinDir, runners[0]);
1841
+ } catch {
1842
+ }
1843
+ return null;
1844
+ }
1737
1845
  function resolveCodexCommand(deps = {}) {
1738
1846
  const pathCommand = resolveCommandOnPath("codex", deps);
1739
1847
  if (pathCommand) return pathCommand;
1740
- if ((deps.platform ?? process.platform) !== "darwin") return null;
1741
- return firstExistingPath([CODEX_DESKTOP_BUNDLE_PATH], deps);
1848
+ const platform = deps.platform ?? process.platform;
1849
+ if (platform === "darwin") {
1850
+ return firstExistingPath([CODEX_DESKTOP_BUNDLE_PATH], deps);
1851
+ }
1852
+ if (platform === "win32") {
1853
+ return resolveWindowsSandboxRunner(deps);
1854
+ }
1855
+ return null;
1742
1856
  }
1743
1857
  function probeCodex(deps = {}) {
1744
1858
  if ((deps.platform ?? process.platform) === "win32") {
@@ -1779,6 +1893,13 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
1779
1893
  }
1780
1894
  }
1781
1895
  if (!codexEntry) {
1896
+ const sandboxRunner = resolveWindowsSandboxRunner(deps);
1897
+ if (sandboxRunner) {
1898
+ return {
1899
+ command: sandboxRunner,
1900
+ args: commandArgs
1901
+ };
1902
+ }
1782
1903
  throw new Error(
1783
1904
  "Cannot resolve Codex CLI entry point on Windows. Ensure @openai/codex is installed globally via npm (npm i -g @openai/codex)."
1784
1905
  );
@@ -2228,6 +2349,15 @@ function detectCodexModels(home = os2.homedir()) {
2228
2349
  import { spawn as spawn3 } from "child_process";
2229
2350
  import path5 from "path";
2230
2351
  import { writeFileSync as writeFileSync3 } from "fs";
2352
+ function buildCopilotSpawnEnv(ctx) {
2353
+ return {
2354
+ ...process.env,
2355
+ FORCE_COLOR: "0",
2356
+ NO_COLOR: "1",
2357
+ ...ctx.config.envVars || {},
2358
+ [SLOCK_HOME_ENV]: resolveSlockHome()
2359
+ };
2360
+ }
2231
2361
  var CopilotDriver = class {
2232
2362
  id = "copilot";
2233
2363
  lifecycle = {
@@ -2286,7 +2416,7 @@ var CopilotDriver = class {
2286
2416
  if (ctx.config.sessionId) {
2287
2417
  args.push(`--resume=${ctx.config.sessionId}`);
2288
2418
  }
2289
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2419
+ const spawnEnv = buildCopilotSpawnEnv(ctx);
2290
2420
  const proc = spawn3("copilot", args, {
2291
2421
  cwd: ctx.workingDirectory,
2292
2422
  stdio: ["pipe", "pipe", "pipe"],
@@ -2383,6 +2513,15 @@ var CopilotDriver = class {
2383
2513
  import { spawn as spawn4, spawnSync } from "child_process";
2384
2514
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2385
2515
  import path6 from "path";
2516
+ function buildCursorSpawnEnv(ctx) {
2517
+ return {
2518
+ ...process.env,
2519
+ FORCE_COLOR: "0",
2520
+ NO_COLOR: "1",
2521
+ ...ctx.config.envVars || {},
2522
+ [SLOCK_HOME_ENV]: resolveSlockHome()
2523
+ };
2524
+ }
2386
2525
  var CursorDriver = class {
2387
2526
  id = "cursor";
2388
2527
  lifecycle = {
@@ -2437,7 +2576,7 @@ var CursorDriver = class {
2437
2576
  args.push("--resume", ctx.config.sessionId);
2438
2577
  }
2439
2578
  args.push(ctx.prompt);
2440
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2579
+ const spawnEnv = buildCursorSpawnEnv(ctx);
2441
2580
  const proc = spawn4("cursor-agent", args, {
2442
2581
  cwd: ctx.workingDirectory,
2443
2582
  stdio: ["pipe", "pipe", "pipe"],
@@ -2772,13 +2911,16 @@ var GeminiDriver = class {
2772
2911
  // src/drivers/kimi.ts
2773
2912
  import { randomUUID } from "crypto";
2774
2913
  import { spawn as spawn6 } from "child_process";
2775
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2914
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2776
2915
  import os3 from "os";
2777
2916
  import path8 from "path";
2778
2917
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
2779
2918
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
2780
2919
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
2781
2920
  var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
2921
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
2922
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
2923
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
2782
2924
  function parseToolArguments(raw) {
2783
2925
  if (typeof raw !== "string") return raw;
2784
2926
  try {
@@ -2787,6 +2929,73 @@ function parseToolArguments(raw) {
2787
2929
  return raw;
2788
2930
  }
2789
2931
  }
2932
+ function readKimiConfigSource(home = os3.homedir(), env = process.env) {
2933
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2934
+ if (inlineConfig && inlineConfig.trim()) {
2935
+ return {
2936
+ raw: inlineConfig,
2937
+ explicitPath: null,
2938
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
2939
+ };
2940
+ }
2941
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
2942
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path8.join(home, ".kimi", "config.toml");
2943
+ try {
2944
+ return {
2945
+ raw: readFileSync3(configPath, "utf8"),
2946
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2947
+ sourcePath: configPath
2948
+ };
2949
+ } catch {
2950
+ return {
2951
+ raw: null,
2952
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
2953
+ sourcePath: configPath
2954
+ };
2955
+ }
2956
+ }
2957
+ function buildKimiSpawnEnv(env = process.env) {
2958
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
2959
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
2960
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
2961
+ return spawnEnv;
2962
+ }
2963
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
2964
+ return {
2965
+ ...process.env,
2966
+ ...ctx.config.envVars || {},
2967
+ ...overrideEnv || {}
2968
+ };
2969
+ }
2970
+ function buildKimiLaunchOptions(ctx, opts = {}) {
2971
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
2972
+ const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
2973
+ const args = [];
2974
+ let configFilePath = null;
2975
+ let configContent = null;
2976
+ if (source.explicitPath) {
2977
+ configFilePath = source.explicitPath;
2978
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
2979
+ configFilePath = path8.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
2980
+ configContent = source.raw;
2981
+ if (opts.writeGeneratedConfig !== false) {
2982
+ writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
2983
+ chmodSync(configFilePath, 384);
2984
+ }
2985
+ }
2986
+ if (configFilePath) {
2987
+ args.push("--config-file", configFilePath);
2988
+ }
2989
+ if (ctx.config.model && ctx.config.model !== "default") {
2990
+ args.push("--model", ctx.config.model);
2991
+ }
2992
+ return {
2993
+ args,
2994
+ env: buildKimiSpawnEnv(env),
2995
+ configFilePath,
2996
+ configContent
2997
+ };
2998
+ }
2790
2999
  var KimiDriver = class {
2791
3000
  id = "kimi";
2792
3001
  lifecycle = {
@@ -2803,7 +3012,25 @@ var KimiDriver = class {
2803
3012
  };
2804
3013
  model = {
2805
3014
  detectedModelsVerifiedAs: "launchable",
2806
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3015
+ toLaunchSpec: (modelId, ctx, opts) => {
3016
+ if (!ctx) return { args: ["--model", modelId] };
3017
+ const launchCtx = {
3018
+ ...ctx,
3019
+ config: {
3020
+ ...ctx.config,
3021
+ model: modelId
3022
+ }
3023
+ };
3024
+ const launch = buildKimiLaunchOptions(launchCtx, {
3025
+ home: opts?.home,
3026
+ writeGeneratedConfig: false
3027
+ });
3028
+ return {
3029
+ args: launch.args,
3030
+ env: launch.env,
3031
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
3032
+ };
3033
+ }
2807
3034
  };
2808
3035
  supportsStdinNotification = true;
2809
3036
  mcpToolPrefix = "";
@@ -2855,6 +3082,7 @@ var KimiDriver = class {
2855
3082
  }
2856
3083
  }
2857
3084
  }), "utf8");
3085
+ const launch = buildKimiLaunchOptions(ctx);
2858
3086
  const args = [
2859
3087
  "--wire",
2860
3088
  "--yolo",
@@ -2863,16 +3091,13 @@ var KimiDriver = class {
2863
3091
  "--mcp-config-file",
2864
3092
  mcpConfigPath,
2865
3093
  "--session",
2866
- this.sessionId
3094
+ this.sessionId,
3095
+ ...launch.args
2867
3096
  ];
2868
- if (ctx.config.model && ctx.config.model !== "default") {
2869
- args.push("--model", ctx.config.model);
2870
- }
2871
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
2872
3097
  const proc = spawn6("kimi", args, {
2873
3098
  cwd: ctx.workingDirectory,
2874
3099
  stdio: ["pipe", "pipe", "pipe"],
2875
- env: spawnEnv,
3100
+ env: launch.env,
2876
3101
  shell: process.platform === "win32"
2877
3102
  });
2878
3103
  proc.stdin?.write(JSON.stringify({
@@ -2989,14 +3214,9 @@ var KimiDriver = class {
2989
3214
  return detectKimiModels();
2990
3215
  }
2991
3216
  };
2992
- function detectKimiModels(home = os3.homedir()) {
2993
- const configPath = path8.join(home, ".kimi", "config.toml");
2994
- let raw;
2995
- try {
2996
- raw = readFileSync3(configPath, "utf8");
2997
- } catch {
2998
- return null;
2999
- }
3217
+ function detectKimiModels(home = os3.homedir(), opts = {}) {
3218
+ const raw = readKimiConfigSource(home, opts.env).raw;
3219
+ if (raw === null) return null;
3000
3220
  const models = [];
3001
3221
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
3002
3222
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -3399,6 +3619,304 @@ var OpenCodeDriver = class {
3399
3619
  }
3400
3620
  };
3401
3621
 
3622
+ // src/drivers/pi.ts
3623
+ import { spawn as spawn8, spawnSync as spawnSync3 } from "child_process";
3624
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
3625
+ import { fileURLToPath } from "url";
3626
+ import path10 from "path";
3627
+ var CHAT_MCP_TOOL_PREFIX2 = "chat_";
3628
+ var NO_MESSAGE_PROMPT2 = "No new messages are pending. Stop now.";
3629
+ var FIRST_MESSAGE_TASK_PREFIX2 = "First message task (system-triggered):";
3630
+ var MIN_BUNDLED_PI_VERSION = "0.74.0";
3631
+ function defaultPackageResolver(specifier) {
3632
+ return import.meta.resolve(specifier);
3633
+ }
3634
+ function parseSemver2(version) {
3635
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
3636
+ if (!match) return null;
3637
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
3638
+ }
3639
+ function isSupportedPiVersion(version) {
3640
+ if (!version) return true;
3641
+ const actual = parseSemver2(version);
3642
+ const minimum = parseSemver2(MIN_BUNDLED_PI_VERSION);
3643
+ if (!actual || !minimum) return true;
3644
+ for (let i = 0; i < 3; i += 1) {
3645
+ if (actual[i] > minimum[i]) return true;
3646
+ if (actual[i] < minimum[i]) return false;
3647
+ }
3648
+ return true;
3649
+ }
3650
+ function resolveBundledPiCliPath(resolver = defaultPackageResolver) {
3651
+ try {
3652
+ const mainPath = fileURLToPath(resolver("@earendil-works/pi-coding-agent"));
3653
+ const cliPath = path10.join(path10.dirname(mainPath), "cli.js");
3654
+ return existsSync7(cliPath) ? cliPath : null;
3655
+ } catch {
3656
+ return null;
3657
+ }
3658
+ }
3659
+ function readBundledPiVersion(cliPath) {
3660
+ const result = spawnSync3(process.execPath, [cliPath, "--version"], {
3661
+ encoding: "utf8",
3662
+ timeout: 5e3
3663
+ });
3664
+ if (result.error || result.status !== 0) return null;
3665
+ return String(result.stdout || result.stderr || "").trim() || null;
3666
+ }
3667
+ function unsupportedPiVersionMessage(version) {
3668
+ if (!version || isSupportedPiVersion(version)) return null;
3669
+ return `Bundled Pi CLI ${version} is unsupported; requires Pi >= ${MIN_BUNDLED_PI_VERSION}.`;
3670
+ }
3671
+ function buildPiLaunchOptions(ctx, opts = {}) {
3672
+ const cliPath = opts.cliPath ?? resolveBundledPiCliPath();
3673
+ if (!cliPath) {
3674
+ throw new Error("Bundled Pi CLI not found in @earendil-works/pi-coding-agent");
3675
+ }
3676
+ const piDir = path10.join(ctx.workingDirectory, ".slock", "pi");
3677
+ const sessionDir = path10.join(piDir, "sessions");
3678
+ mkdirSync4(sessionDir, { recursive: true });
3679
+ const slock = prepareCliTransport(ctx, {
3680
+ NO_COLOR: "1",
3681
+ PI_CODING_AGENT_DIR: piDir,
3682
+ PI_CODING_AGENT_SESSION_DIR: sessionDir
3683
+ });
3684
+ const systemPromptPath = path10.join(slock.slockDir, "pi-system-prompt.md");
3685
+ writeFileSync7(systemPromptPath, ctx.standingPrompt, { encoding: "utf8", mode: 384 });
3686
+ const args = [
3687
+ cliPath,
3688
+ "--mode",
3689
+ "json",
3690
+ "--session-dir",
3691
+ sessionDir,
3692
+ "--append-system-prompt",
3693
+ systemPromptPath,
3694
+ "--no-context-files",
3695
+ "--no-extensions",
3696
+ "--no-skills",
3697
+ "--no-prompt-templates",
3698
+ "--no-themes"
3699
+ ];
3700
+ if (ctx.config.model && ctx.config.model !== "default") {
3701
+ args.push("--model", ctx.config.model);
3702
+ }
3703
+ if (ctx.config.sessionId) {
3704
+ args.push("--session", ctx.config.sessionId);
3705
+ }
3706
+ const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT2 : ctx.prompt;
3707
+ args.push(turnPrompt);
3708
+ return {
3709
+ command: process.execPath,
3710
+ args,
3711
+ env: slock.spawnEnv,
3712
+ sessionDir,
3713
+ systemPromptPath
3714
+ };
3715
+ }
3716
+ function isSystemFirstMessageTask2(message) {
3717
+ return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX2);
3718
+ }
3719
+ function buildPiSystemPrompt(config) {
3720
+ return buildCliTransportSystemPrompt(config, {
3721
+ toolPrefix: CHAT_MCP_TOOL_PREFIX2,
3722
+ extraCriticalRules: [
3723
+ "- Runtime Profile migration controls are not available in the Pi runtime yet. If asked to acknowledge a runtime migration, explain the blocker instead of inventing a command."
3724
+ ],
3725
+ postStartupNotes: [
3726
+ "**Pi 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."
3727
+ ],
3728
+ includeStdinNotificationSection: false,
3729
+ messageNotificationStyle: "poll"
3730
+ });
3731
+ }
3732
+ function contentText(content) {
3733
+ if (!content) return "";
3734
+ const chunks = [];
3735
+ for (const item of content) {
3736
+ if (item.type === "text" && typeof item.text === "string") {
3737
+ chunks.push(item.text);
3738
+ }
3739
+ }
3740
+ return chunks.join("");
3741
+ }
3742
+ function apiKeyErrorMessage(line) {
3743
+ const trimmed = line.trim();
3744
+ if (!trimmed) return null;
3745
+ if (/no api key found/i.test(trimmed)) return trimmed;
3746
+ if (/api key.+required/i.test(trimmed)) return trimmed;
3747
+ if (/no models available/i.test(trimmed)) return trimmed;
3748
+ return null;
3749
+ }
3750
+ var PiDriver = class {
3751
+ id = "pi";
3752
+ lifecycle = {
3753
+ kind: "per_turn",
3754
+ start: "defer_until_concrete_message",
3755
+ exit: "terminate_on_turn_end",
3756
+ inFlightWake: "coalesce_into_pending"
3757
+ };
3758
+ communication = {
3759
+ chat: "slock_cli",
3760
+ runtimeControl: "none"
3761
+ };
3762
+ session = {
3763
+ recovery: "resume_or_fresh"
3764
+ };
3765
+ model = {
3766
+ detectedModelsVerifiedAs: "launchable",
3767
+ toLaunchSpec: (modelId, ctx) => {
3768
+ if (!ctx) return { args: ["--model", modelId] };
3769
+ const launchCtx = {
3770
+ ...ctx,
3771
+ config: {
3772
+ ...ctx.config,
3773
+ model: modelId
3774
+ }
3775
+ };
3776
+ const launch = buildPiLaunchOptions(launchCtx);
3777
+ return {
3778
+ args: launch.args,
3779
+ env: launch.env,
3780
+ configFiles: [launch.systemPromptPath]
3781
+ };
3782
+ }
3783
+ };
3784
+ supportsStdinNotification = false;
3785
+ mcpToolPrefix = CHAT_MCP_TOOL_PREFIX2;
3786
+ busyDeliveryMode = "none";
3787
+ terminateProcessOnTurnEnd = true;
3788
+ deferSpawnUntilMessage = true;
3789
+ usesSlockCliForCommunication = true;
3790
+ sessionId = null;
3791
+ sessionAnnounced = false;
3792
+ apiKeyErrorAnnounced = false;
3793
+ turnEnded = false;
3794
+ assistantTextByMessageId = /* @__PURE__ */ new Map();
3795
+ shouldDeferWakeMessage(message) {
3796
+ return isSystemFirstMessageTask2(message);
3797
+ }
3798
+ probe() {
3799
+ const cliPath = resolveBundledPiCliPath();
3800
+ if (!cliPath) return { available: false };
3801
+ const version = readBundledPiVersion(cliPath) || void 0;
3802
+ const unsupportedMessage = unsupportedPiVersionMessage(version);
3803
+ if (unsupportedMessage) {
3804
+ return {
3805
+ available: false,
3806
+ version: `${version} (requires >= ${MIN_BUNDLED_PI_VERSION})`
3807
+ };
3808
+ }
3809
+ return { available: true, version };
3810
+ }
3811
+ async detectModels() {
3812
+ return null;
3813
+ }
3814
+ spawn(ctx) {
3815
+ this.sessionId = ctx.config.sessionId || null;
3816
+ this.sessionAnnounced = false;
3817
+ this.apiKeyErrorAnnounced = false;
3818
+ this.turnEnded = false;
3819
+ this.assistantTextByMessageId.clear();
3820
+ const cliPath = resolveBundledPiCliPath();
3821
+ if (!cliPath) throw new Error("Bundled Pi CLI not found in @earendil-works/pi-coding-agent");
3822
+ const unsupportedMessage = unsupportedPiVersionMessage(readBundledPiVersion(cliPath));
3823
+ if (unsupportedMessage) throw new Error(unsupportedMessage);
3824
+ const launch = buildPiLaunchOptions(ctx, { cliPath });
3825
+ const proc = spawn8(launch.command, launch.args, {
3826
+ cwd: ctx.workingDirectory,
3827
+ stdio: ["pipe", "pipe", "pipe"],
3828
+ env: launch.env,
3829
+ shell: process.platform === "win32"
3830
+ });
3831
+ proc.stdin?.end();
3832
+ return { process: proc };
3833
+ }
3834
+ parseLine(line) {
3835
+ let event;
3836
+ try {
3837
+ event = JSON.parse(line);
3838
+ } catch {
3839
+ if (this.apiKeyErrorAnnounced) return [];
3840
+ const message = apiKeyErrorMessage(line);
3841
+ if (!message) return [];
3842
+ this.apiKeyErrorAnnounced = true;
3843
+ this.turnEnded = true;
3844
+ return [
3845
+ { kind: "error", message },
3846
+ { kind: "turn_end", sessionId: this.sessionId || void 0 }
3847
+ ];
3848
+ }
3849
+ const events = [];
3850
+ if (event.type === "session" && event.id) {
3851
+ this.sessionId = event.id;
3852
+ }
3853
+ if (!this.sessionAnnounced && this.sessionId) {
3854
+ events.push({ kind: "session_init", sessionId: this.sessionId });
3855
+ this.sessionAnnounced = true;
3856
+ }
3857
+ switch (event.type) {
3858
+ case "agent_start":
3859
+ case "turn_start":
3860
+ events.push({ kind: "thinking", text: "" });
3861
+ break;
3862
+ case "message_update":
3863
+ case "message_end":
3864
+ if (event.message?.role === "assistant") {
3865
+ const key = event.message.id || "current";
3866
+ const currentText = contentText(event.message.content);
3867
+ const previousText = this.assistantTextByMessageId.get(key) ?? "";
3868
+ if (currentText.length > previousText.length && currentText.startsWith(previousText)) {
3869
+ events.push({ kind: "text", text: currentText.slice(previousText.length) });
3870
+ } else if (currentText && currentText !== previousText) {
3871
+ events.push({ kind: "text", text: currentText });
3872
+ }
3873
+ this.assistantTextByMessageId.set(key, currentText);
3874
+ if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
3875
+ events.push({ kind: "error", message: event.message.errorMessage || `Request ${event.message.stopReason}` });
3876
+ }
3877
+ }
3878
+ break;
3879
+ case "tool_execution_start":
3880
+ events.push({
3881
+ kind: "tool_call",
3882
+ name: event.toolName || "unknown_tool",
3883
+ input: event.args
3884
+ });
3885
+ break;
3886
+ case "tool_execution_end":
3887
+ events.push({
3888
+ kind: "tool_output",
3889
+ name: event.toolName || "unknown_tool"
3890
+ });
3891
+ if (event.isError) {
3892
+ events.push({ kind: "error", message: `Pi tool ${event.toolName || "unknown_tool"} failed` });
3893
+ }
3894
+ break;
3895
+ case "compaction_start":
3896
+ events.push({ kind: "compaction_started" });
3897
+ break;
3898
+ case "compaction_end":
3899
+ events.push({ kind: "compaction_finished" });
3900
+ if (event.errorMessage) events.push({ kind: "error", message: event.errorMessage });
3901
+ break;
3902
+ case "turn_end":
3903
+ case "agent_end":
3904
+ if (!this.turnEnded) {
3905
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
3906
+ this.turnEnded = true;
3907
+ }
3908
+ break;
3909
+ }
3910
+ return events;
3911
+ }
3912
+ encodeStdinMessage(_text, _sessionId, _opts) {
3913
+ return null;
3914
+ }
3915
+ buildSystemPrompt(config, _agentId) {
3916
+ return buildPiSystemPrompt(config);
3917
+ }
3918
+ };
3919
+
3402
3920
  // src/drivers/index.ts
3403
3921
  var driverFactories = {
3404
3922
  claude: () => new ClaudeDriver(),
@@ -3407,7 +3925,8 @@ var driverFactories = {
3407
3925
  cursor: () => new CursorDriver(),
3408
3926
  gemini: () => new GeminiDriver(),
3409
3927
  kimi: () => new KimiDriver(),
3410
- opencode: () => new OpenCodeDriver()
3928
+ opencode: () => new OpenCodeDriver(),
3929
+ pi: () => new PiDriver()
3411
3930
  };
3412
3931
  function getDriver(runtimeId) {
3413
3932
  const createDriver = driverFactories[runtimeId];
@@ -3420,7 +3939,7 @@ function getDriver(runtimeId) {
3420
3939
 
3421
3940
  // src/workspaces.ts
3422
3941
  import { readdir, rm, stat } from "fs/promises";
3423
- import path10 from "path";
3942
+ import path11 from "path";
3424
3943
  function isValidWorkspaceDirectoryName(directoryName) {
3425
3944
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
3426
3945
  }
@@ -3428,7 +3947,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
3428
3947
  if (!isValidWorkspaceDirectoryName(directoryName)) {
3429
3948
  return null;
3430
3949
  }
3431
- return path10.join(dataDir, directoryName);
3950
+ return path11.join(dataDir, directoryName);
3432
3951
  }
3433
3952
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
3434
3953
  return {
@@ -3477,7 +3996,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
3477
3996
  return summary;
3478
3997
  }
3479
3998
  const childSummaries = await Promise.all(
3480
- entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
3999
+ entries.map((entry) => summarizeWorkspaceEntry(path11.join(dirPath, entry.name), entry))
3481
4000
  );
3482
4001
  for (const childSummary of childSummaries) {
3483
4002
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -3496,7 +4015,7 @@ async function scanWorkspaceDirectories(dataDir) {
3496
4015
  if (!entry.isDirectory()) {
3497
4016
  return null;
3498
4017
  }
3499
- const dirPath = path10.join(dataDir, entry.name);
4018
+ const dirPath = path11.join(dataDir, entry.name);
3500
4019
  try {
3501
4020
  const summary = await summarizeWorkspaceDirectory(dirPath);
3502
4021
  return {
@@ -3527,8 +4046,86 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3527
4046
  }
3528
4047
  }
3529
4048
 
4049
+ // src/runtimeErrorDiagnostics.ts
4050
+ import { createHash } from "crypto";
4051
+ var MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS = 4096;
4052
+ function buildRuntimeErrorDiagnosticEnvelope(message) {
4053
+ const rawMessage = String(message || "");
4054
+ const scrubbed = scrubRuntimeErrorDiagnosticText(rawMessage);
4055
+ const { value: excerpt, truncated } = truncateDiagnosticText(scrubbed, MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS);
4056
+ const httpStatus = extractHttpStatus(rawMessage);
4057
+ const runtimeErrorClass = classifyRuntimeError(rawMessage, httpStatus);
4058
+ const fingerprint = fingerprintRuntimeError(scrubbed);
4059
+ const spanAttrs = {
4060
+ runtime_error_class: runtimeErrorClass,
4061
+ runtime_error_fingerprint: fingerprint,
4062
+ runtime_error_message_present: rawMessage.length > 0,
4063
+ runtime_error_message_length_bucket: bucketLength(rawMessage.length),
4064
+ runtime_error_message_truncated: truncated
4065
+ };
4066
+ if (httpStatus !== null) {
4067
+ spanAttrs.runtime_error_http_status = httpStatus;
4068
+ }
4069
+ return {
4070
+ spanAttrs,
4071
+ eventAttrs: {
4072
+ ...spanAttrs,
4073
+ runtime_error_message_excerpt: excerpt
4074
+ }
4075
+ };
4076
+ }
4077
+ function scrubRuntimeErrorDiagnosticText(value) {
4078
+ return value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED_TOKEN]").replace(/\b(?:sk|sk-ant|sk-proj|xox[baprs]?)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED_TOKEN]").replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]").replace(/https?:\/\/[^\s"'<>]+/gi, (url) => redactUrlQuery(url)).replace(/\/Users\/[^\s"'<>:]+(?:\/[^\s"'<>:]+)*/g, "[REDACTED_PATH]").replace(/\/home\/[^\s"'<>:]+(?:\/[^\s"'<>:]+)*/g, "[REDACTED_PATH]").replace(/[A-Za-z]:\\Users\\[^\s"'<>:]+(?:\\[^\s"'<>:]+)*/g, "[REDACTED_PATH]");
4079
+ }
4080
+ function truncateDiagnosticText(value, maxChars) {
4081
+ if (value.length <= maxChars) return { value, truncated: false };
4082
+ return { value: value.slice(0, maxChars), truncated: true };
4083
+ }
4084
+ function extractHttpStatus(message) {
4085
+ const match = /\b(?:HTTP|status(?:\s+code)?|API\s+Error)[:\s]+([45]\d{2})\b/i.exec(message) ?? /\b([45]\d{2})\s+(?:Bad Request|Unauthorized|Forbidden|Not Found|Conflict|Too Many Requests|Internal Server Error|Service Unavailable)\b/i.exec(message);
4086
+ if (!match) return null;
4087
+ const status = Number(match[1]);
4088
+ return Number.isInteger(status) ? status : null;
4089
+ }
4090
+ function classifyRuntimeError(message, httpStatus) {
4091
+ const explicit = /\b([A-Z][A-Za-z0-9_]*(?:Error|Exception))\b/.exec(message);
4092
+ if (explicit) return explicit[1];
4093
+ if (httpStatus !== null) {
4094
+ if (httpStatus === 429) return "RateLimitError";
4095
+ if (httpStatus === 401 || httpStatus === 403) return "AuthError";
4096
+ if (httpStatus === 404) return "NotFoundError";
4097
+ if (httpStatus >= 500) return "ProviderServerError";
4098
+ return "ProviderApiError";
4099
+ }
4100
+ if (/\btimeout|timed out\b/i.test(message)) return "TimeoutError";
4101
+ if (/\brate.?limit|too many requests\b/i.test(message)) return "RateLimitError";
4102
+ if (/\bnot found\b/i.test(message)) return "NotFoundError";
4103
+ return "RuntimeError";
4104
+ }
4105
+ function fingerprintRuntimeError(value) {
4106
+ const normalized = value.toLowerCase().replace(/[0-9a-f]{12,}/g, "<hex>").replace(/\b\d+\b/g, "<num>").replace(/\s+/g, " ").trim();
4107
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
4108
+ }
4109
+ function bucketLength(length) {
4110
+ if (length === 0) return "0";
4111
+ if (length < 1024) return "<1k";
4112
+ if (length < 4096) return "1k-4k";
4113
+ if (length < 16384) return "4k-16k";
4114
+ return "16k+";
4115
+ }
4116
+ function redactUrlQuery(value) {
4117
+ try {
4118
+ const url = new URL(value);
4119
+ if (url.search) url.search = "?[REDACTED_QUERY]";
4120
+ if (url.username) url.username = "[REDACTED_USER]";
4121
+ if (url.password) url.password = "[REDACTED_PASSWORD]";
4122
+ return url.toString();
4123
+ } catch {
4124
+ return value.replace(/\?.*$/, "?[REDACTED_QUERY]");
4125
+ }
4126
+ }
4127
+
3530
4128
  // src/agentProcessManager.ts
3531
- var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3532
4129
  var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3533
4130
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3534
4131
  var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
@@ -3609,19 +4206,19 @@ function findSessionJsonl(root, predicate) {
3609
4206
  if (depth < 0 || visited >= maxEntries) return null;
3610
4207
  let entries;
3611
4208
  try {
3612
- entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
4209
+ entries = readdirSync3(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
3613
4210
  } catch {
3614
4211
  return null;
3615
4212
  }
3616
4213
  for (const entry of entries) {
3617
4214
  if (++visited > maxEntries) return null;
3618
4215
  if (!entry.isFile() || !predicate(entry.name)) continue;
3619
- return path11.join(dir, entry.name);
4216
+ return path12.join(dir, entry.name);
3620
4217
  }
3621
4218
  for (const entry of entries) {
3622
4219
  if (++visited > maxEntries) return null;
3623
4220
  if (!entry.isDirectory()) continue;
3624
- const found = visit(path11.join(dir, entry.name), depth - 1);
4221
+ const found = visit(path12.join(dir, entry.name), depth - 1);
3625
4222
  if (found) return found;
3626
4223
  }
3627
4224
  return null;
@@ -3634,10 +4231,10 @@ function safeSessionFilename(value) {
3634
4231
  }
3635
4232
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3636
4233
  try {
3637
- const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
3638
- mkdirSync4(dir, { recursive: true });
3639
- const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3640
- writeFileSync7(filePath, JSON.stringify({
4234
+ const dir = path12.join(fallbackDir, ".slock", "runtime-sessions");
4235
+ mkdirSync5(dir, { recursive: true });
4236
+ const filePath = path12.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
4237
+ writeFileSync8(filePath, JSON.stringify({
3641
4238
  type: "runtime_session_handoff",
3642
4239
  runtime,
3643
4240
  sessionId,
@@ -3656,7 +4253,7 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
3656
4253
  }
3657
4254
  }
3658
4255
  function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
3659
- const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
4256
+ const directPath = path12.isAbsolute(sessionId) ? sessionId : null;
3660
4257
  if (directPath) {
3661
4258
  try {
3662
4259
  if (statSync2(directPath).isFile()) {
@@ -3665,7 +4262,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), f
3665
4262
  } catch {
3666
4263
  }
3667
4264
  }
3668
- const resolvedPath = runtime === "claude" ? findSessionJsonl(path11.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path11.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
4265
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path12.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path12.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
3669
4266
  if (!resolvedPath && fallbackDir) {
3670
4267
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
3671
4268
  if (fallback) return fallback;
@@ -3857,15 +4454,27 @@ After confirming language preference, do not give a generic product introduction
3857
4454
  - Use a low-pressure prompt and guide to one concrete starter action.
3858
4455
 
3859
4456
  ### Starter Plan Output
3860
- A starter plan should make the next action executable, not just descriptive:
3861
- - agent name + role description that can be copied into the create-agent form
3862
- - suggested channel or workstream
3863
- - first task to send after creation
3864
- - next UI action if the user needs to create an agent or channel
4457
+ A starter plan should make the next action executable, not just descriptive.
4458
+ For new channels, new agents, and adding members to an existing channel, post an **action card** rather than a copyable spec:
4459
+
4460
+ - Use \`slock action prepare --target <onboarding-channel>\` and pipe an \`ActionCardAction\` JSON. Identity references are handles (\`@alice\` / \`@scout\` / \`#general\` \u2014 bare names work too), never UUIDs. Server resolves at prepare time.
4461
+ - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
4462
+ - \`{type: "agent:create", name, description?, draftHint?}\`
4463
+ - \`{type: "channel:add_member", channel: "#existing-channel", humans?: ["@alice"], agents?: ["@scout"], draftHint?}\` \u2014 at least one of humans / agents must be non-empty
4464
+ - The owner clicks the button on the card; the matching dialog opens **prefilled with your values** (editable, deselectable for add_member). They review, adjust, and submit; the action is committed under their identity.
4465
+ - Technical fields the owner must pick themselves are NOT yours to prefill on \`agent:create\`: computer, runtime, model, reasoning effort. Stay on semantic intent (name + description).
4466
+ - For \`channel:add_member\`, only suggest people who are actually likely candidates (already in the server, relevant to the channel's topic). The owner will deselect anyone they don't want \u2014 make their default-yes list useful, not exhaustive.
4467
+ - Do not just describe or list copyable specs once action cards are available \u2014 the human input cost should land at "click the card, review, submit", not "copy this name into the dialog yourself".
4468
+ - Do not imply the resource has been created or members added until the card flips to "Done".
4469
+
4470
+ Other plan elements still apply:
4471
+ - suggested channel or workstream pairing
4472
+ - first task to send after the card is committed
4473
+ - who to pull in once the channel exists (often a follow-up \`channel:add_member\` card)
3865
4474
 
3866
4475
  Do not use a rigid keyword routing table. Use examples as inspiration, then adapt to the user's context.
3867
- If details are missing but not blocking, state reasonable defaults and invite correction.
3868
- Only ask one blocking question first if the answer is required before any useful starter plan can be proposed.
4476
+ If details are missing but not blocking, state reasonable defaults inside the action card payload (the owner can edit) and invite correction in chat.
4477
+ Only ask one blocking question first if the answer is required before any useful card can be prepared.
3869
4478
  Do not imply you have already created agents or channels unless the action has actually happened.
3870
4479
 
3871
4480
  ### Capability Boundary Pivot
@@ -4103,16 +4712,22 @@ Do not copy these answers verbatim.
4103
4712
 
4104
4713
  ## FAQ 15: How do I create agents or channels?
4105
4714
  ### Answer idea
4106
- - The user can create agents from the Agents section in the People tab by clicking the + button, or from a computer/machine context menu via Create Agent.
4107
- - The user can create channels from the Channels section by clicking the + button.
4108
- - When recommending setup, provide copyable agent names/descriptions, suggested channel names, and the first task to send after creation.
4715
+ - When the owner agrees to a new agent or channel, **post an action card** with \`slock action prepare\`. The card lives inline in chat; the owner clicks the action button, the matching create dialog opens prefilled with your values (editable), and the resource is created under their identity when they submit.
4716
+ - v1 supports three action types via \`slock action prepare --target '<channel>' <<EOF { ... } EOF\`:
4717
+ - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
4718
+ - \`{type: "agent:create", name, description?, draftHint?}\` \u2014 runtime / model / computer are the owner's call, not yours
4719
+ - \`{type: "channel:add_member", channel: "#existing-channel", humans?: ["@alice"], agents?: ["@scout"], draftHint?}\` \u2014 at least one of humans / agents must be non-empty. The owner clicks "Add Members" on the card; an AddMembers dialog opens with your suggested list (each row toggleable) and the owner submits to actually add them.
4720
+
4721
+ - **Identity references are handles, not UUIDs.** Use \`@alice\` / \`@scout\` / \`#general\` (or bare \`alice\` / \`scout\` / \`general\`). The server resolves to UUIDs at prepare time. If a handle doesn't match a real human / agent / channel in this server you get a 422 INVALID_HANDLE error pointing at the field \u2014 fix the handle and retry. You should never see or write UUIDs in action card payloads.
4722
+ - Manual fallback (only when the owner explicitly wants to do it themselves or asks how to repeat it): the + buttons in the Agents and Channels sidebar sections. Lead with the action card; mention the + button only on request.
4109
4723
 
4110
4724
  ### Next step
4111
- - Give the exact agent/channel spec the user can copy, then guide them to the relevant + button or creation dialog.
4725
+ - Prefill the values you have, post the action card with a short \`draftHint\` explaining why these values, and tell the owner: "click the button on the card to review and commit." Then propose the first task to send once the card flips to Done.
4112
4726
 
4113
4727
  ### Guardrail
4114
- - Do not imply you already created agents or channels unless that action actually happened.
4115
- - If you cannot directly create something, avoid a long permissions explanation; give the copyable spec and the next UI action.
4728
+ - Do not imply you already created agents or channels unless the card state is \`executed\`.
4729
+ - Do not prefill technical fields on \`agent:create\` (runtime / model / computer / reasoning effort). The owner picks those in the dialog.
4730
+ - If the action type the user wants is not yet supported (e.g. \`channel:add_member\`), say so plainly and offer the manual UI path; do not invent action types the schema does not accept.
4116
4731
  `;
4117
4732
  }
4118
4733
  function buildOnboardingSeedFiles() {
@@ -4212,6 +4827,20 @@ function runtimeProfileNotificationFromMessage(message) {
4212
4827
  function runtimeProfileNotificationTitle(kind) {
4213
4828
  return kind === "migration" ? "Runtime Profile migration" : "Runtime Profile notice";
4214
4829
  }
4830
+ function hashRuntimeProfileKey(key) {
4831
+ if (!key) return void 0;
4832
+ return createHash2("sha256").update(key).digest("hex").slice(0, 16);
4833
+ }
4834
+ function runtimeProfileTurnControl(kind, key, source) {
4835
+ return {
4836
+ kind,
4837
+ source,
4838
+ keyHash: hashRuntimeProfileKey(key) ?? null,
4839
+ keyPresent: Boolean(key),
4840
+ injectedAtMs: Date.now(),
4841
+ migrationDoneToolObserved: false
4842
+ };
4843
+ }
4215
4844
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
4216
4845
  const next = [...lines];
4217
4846
  for (const rawLine of chunk.split(/\r?\n/)) {
@@ -4400,13 +5029,14 @@ var AgentProcessManager = class _AgentProcessManager {
4400
5029
  defaultAgentEnvVarsProvider;
4401
5030
  tracer;
4402
5031
  deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
5032
+ processExitTraceAttrs = /* @__PURE__ */ new WeakMap();
4403
5033
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
4404
5034
  this.chatBridgePath = chatBridgePath;
4405
5035
  this.slockCliPath = opts.slockCliPath ?? "";
4406
5036
  this.sendToServer = sendToServer;
4407
5037
  this.daemonApiKey = daemonApiKey;
4408
5038
  this.serverUrl = opts.serverUrl;
4409
- this.dataDir = opts.dataDir || DATA_DIR;
5039
+ this.dataDir = opts.dataDir || resolveSlockHomePath("agents");
4410
5040
  this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
4411
5041
  this.driverResolver = opts.driverResolver || getDriver;
4412
5042
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
@@ -4651,26 +5281,26 @@ var AgentProcessManager = class _AgentProcessManager {
4651
5281
  this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
4652
5282
  try {
4653
5283
  const driver = this.driverResolver(config.runtime || "claude");
4654
- const agentDataDir = path11.join(this.dataDir, agentId);
5284
+ const agentDataDir = path12.join(this.dataDir, agentId);
4655
5285
  await mkdir(agentDataDir, { recursive: true });
4656
5286
  const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
4657
- const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
5287
+ const memoryMdPath = path12.join(agentDataDir, "MEMORY.md");
4658
5288
  try {
4659
5289
  await access(memoryMdPath);
4660
5290
  } catch {
4661
5291
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
4662
5292
  await writeFile(memoryMdPath, initialMemoryMd);
4663
5293
  }
4664
- const notesDir = path11.join(agentDataDir, "notes");
5294
+ const notesDir = path12.join(agentDataDir, "notes");
4665
5295
  await mkdir(notesDir, { recursive: true });
4666
5296
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
4667
5297
  const seedFiles = buildOnboardingSeedFiles();
4668
5298
  for (const { relativePath, content } of seedFiles) {
4669
- const fullPath = path11.join(agentDataDir, relativePath);
5299
+ const fullPath = path12.join(agentDataDir, relativePath);
4670
5300
  try {
4671
5301
  await access(fullPath);
4672
5302
  } catch {
4673
- await mkdir(path11.dirname(fullPath), { recursive: true });
5303
+ await mkdir(path12.dirname(fullPath), { recursive: true });
4674
5304
  await writeFile(fullPath, content);
4675
5305
  }
4676
5306
  }
@@ -4798,6 +5428,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4798
5428
  exitCode: null,
4799
5429
  exitSignal: null,
4800
5430
  expectedTerminationReason: null,
5431
+ runtimeProfileTurnControl: runtimeConfig.runtimeProfileControl ? runtimeProfileTurnControl(runtimeConfig.runtimeProfileControl.kind, runtimeConfig.runtimeProfileControl.key, "agent_config") : null,
4801
5432
  pendingTrajectory: null,
4802
5433
  gatedSteering: createGatedSteeringState()
4803
5434
  };
@@ -4872,7 +5503,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4872
5503
  clean_exit: code === 0,
4873
5504
  runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4874
5505
  inbox_count: current?.inbox.length ?? 0,
4875
- pending_notification_count: current?.pendingNotificationCount ?? 0
5506
+ pending_notification_count: current?.pendingNotificationCount ?? 0,
5507
+ ...this.processExitTraceAttrs.get(proc)
4876
5508
  });
4877
5509
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4878
5510
  });
@@ -4900,7 +5532,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4900
5532
  outcome: processEndedCleanly ? "process-exit" : "process-crash",
4901
5533
  expectedTerminationReason: ap.expectedTerminationReason || void 0,
4902
5534
  exitCode: finalCode,
4903
- exitSignal: finalSignal
5535
+ exitSignal: finalSignal,
5536
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "process_exit")
4904
5537
  });
4905
5538
  if (processEndedCleanly) {
4906
5539
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
@@ -5063,6 +5696,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5063
5696
  agentId,
5064
5697
  kind,
5065
5698
  key_present: Boolean(key),
5699
+ key_hash: hashRuntimeProfileKey(key),
5066
5700
  outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5067
5701
  runtime: ap.config.runtime,
5068
5702
  session_id_present: Boolean(ap.sessionId),
@@ -5085,6 +5719,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5085
5719
  agentId,
5086
5720
  kind,
5087
5721
  key_present: Boolean(key),
5722
+ key_hash: hashRuntimeProfileKey(key),
5088
5723
  outcome: "queued_during_start",
5089
5724
  startup_pending: true,
5090
5725
  starting_inbox_count: pending.length,
@@ -5121,6 +5756,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5121
5756
  }
5122
5757
  this.clearCompactionWatchdog(ap);
5123
5758
  this.agents.delete(agentId);
5759
+ this.processExitTraceAttrs.set(ap.process, {
5760
+ stop_source: silent ? "daemon_internal" : "explicit_request",
5761
+ stop_wait_requested: wait,
5762
+ stop_silent: silent
5763
+ });
5124
5764
  ap.process.kill("SIGTERM");
5125
5765
  if (!silent) {
5126
5766
  this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
@@ -5320,7 +5960,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5320
5960
  return true;
5321
5961
  }
5322
5962
  async resetWorkspace(agentId) {
5323
- const agentDataDir = path11.join(this.dataDir, agentId);
5963
+ const agentDataDir = path12.join(this.dataDir, agentId);
5324
5964
  try {
5325
5965
  await rm2(agentDataDir, { recursive: true, force: true });
5326
5966
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -5357,7 +5997,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5357
5997
  return result;
5358
5998
  }
5359
5999
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
5360
- const workspacePath = path11.join(this.dataDir, agentId);
6000
+ const workspacePath = path12.join(this.dataDir, agentId);
5361
6001
  return {
5362
6002
  agentId,
5363
6003
  launchId,
@@ -5411,7 +6051,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5411
6051
  attrs: {
5412
6052
  agentId,
5413
6053
  control_kind: kind,
5414
- key_present: Boolean(key)
6054
+ key_present: Boolean(key),
6055
+ key_hash: hashRuntimeProfileKey(key)
5415
6056
  }
5416
6057
  });
5417
6058
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5444,7 +6085,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5444
6085
  }
5445
6086
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
5446
6087
  ap.isIdle = false;
5447
- this.startRuntimeTrace(agentId, ap, "runtime-profile");
6088
+ this.startRuntimeTrace(agentId, ap, "runtime-profile", [message]);
5448
6089
  const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5449
6090
  span.end(written ? "ok" : "error", {
5450
6091
  attrs: {
@@ -5539,6 +6180,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5539
6180
  agentId,
5540
6181
  control_kind: control.kind,
5541
6182
  key_present: Boolean(control.key),
6183
+ key_hash: hashRuntimeProfileKey(control.key),
5542
6184
  launchId: launchId || void 0,
5543
6185
  source: "agent_config"
5544
6186
  }
@@ -5603,7 +6245,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5603
6245
  }
5604
6246
  // Workspace file browsing
5605
6247
  async getFileTree(agentId, dirPath) {
5606
- const agentDir = path11.join(this.dataDir, agentId);
6248
+ const agentDir = path12.join(this.dataDir, agentId);
5607
6249
  try {
5608
6250
  await stat2(agentDir);
5609
6251
  } catch {
@@ -5611,8 +6253,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5611
6253
  }
5612
6254
  let targetDir = agentDir;
5613
6255
  if (dirPath) {
5614
- const resolved = path11.resolve(agentDir, dirPath);
5615
- if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
6256
+ const resolved = path12.resolve(agentDir, dirPath);
6257
+ if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
5616
6258
  return [];
5617
6259
  }
5618
6260
  targetDir = resolved;
@@ -5620,14 +6262,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5620
6262
  return this.listDirectoryChildren(targetDir, agentDir);
5621
6263
  }
5622
6264
  async readFile(agentId, filePath) {
5623
- const agentDir = path11.join(this.dataDir, agentId);
5624
- const resolved = path11.resolve(agentDir, filePath);
5625
- if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
6265
+ const agentDir = path12.join(this.dataDir, agentId);
6266
+ const resolved = path12.resolve(agentDir, filePath);
6267
+ if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
5626
6268
  throw new Error("Access denied");
5627
6269
  }
5628
6270
  const info = await stat2(resolved);
5629
6271
  if (info.isDirectory()) throw new Error("Cannot read a directory");
5630
- const ext = path11.extname(resolved).toLowerCase();
6272
+ const ext = path12.extname(resolved).toLowerCase();
5631
6273
  if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
5632
6274
  if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
5633
6275
  const content = await readFile(resolved, "utf-8");
@@ -5662,13 +6304,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5662
6304
  const agent = this.agents.get(agentId);
5663
6305
  const runtime = runtimeHint || agent?.config.runtime || "claude";
5664
6306
  const home = os5.homedir();
5665
- const workspaceDir = path11.join(this.dataDir, agentId);
6307
+ const workspaceDir = path12.join(this.dataDir, agentId);
5666
6308
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
5667
6309
  const globalResults = await Promise.all(
5668
- paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
6310
+ paths.global.map((p) => this.scanSkillsDir(path12.join(home, p)))
5669
6311
  );
5670
6312
  const workspaceResults = await Promise.all(
5671
- paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
6313
+ paths.workspace.map((p) => this.scanSkillsDir(path12.join(workspaceDir, p)))
5672
6314
  );
5673
6315
  const dedup = (skills) => {
5674
6316
  const seen = /* @__PURE__ */ new Set();
@@ -5697,7 +6339,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5697
6339
  const skills = [];
5698
6340
  for (const entry of entries) {
5699
6341
  if (entry.isDirectory() || entry.isSymbolicLink()) {
5700
- const skillMd = path11.join(dir, entry.name, "SKILL.md");
6342
+ const skillMd = path12.join(dir, entry.name, "SKILL.md");
5701
6343
  try {
5702
6344
  const content = await readFile(skillMd, "utf-8");
5703
6345
  const skill = this.parseSkillMd(entry.name, content);
@@ -5708,7 +6350,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5708
6350
  } else if (entry.name.endsWith(".md")) {
5709
6351
  const cmdName = entry.name.replace(/\.md$/, "");
5710
6352
  try {
5711
- const content = await readFile(path11.join(dir, entry.name), "utf-8");
6353
+ const content = await readFile(path12.join(dir, entry.name), "utf-8");
5712
6354
  const skill = this.parseSkillMd(cmdName, content);
5713
6355
  skill.sourcePath = dir;
5714
6356
  skills.push(skill);
@@ -5903,7 +6545,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5903
6545
  deliveryId: context.deliveryId,
5904
6546
  delivery_correlation_id: context.deliveryId,
5905
6547
  control_kind: notification.kind,
5906
- key_present: Boolean(notification.key)
6548
+ key_present: Boolean(notification.key),
6549
+ runtime_profile_control_kind: notification.kind,
6550
+ runtime_profile_key_hash: hashRuntimeProfileKey(notification.key),
6551
+ runtime_profile_key_present: Boolean(notification.key)
5907
6552
  };
5908
6553
  }
5909
6554
  return {
@@ -5914,8 +6559,63 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5914
6559
  delivery_correlation_id: context.deliveryId ?? first.message_id
5915
6560
  };
5916
6561
  }
6562
+ runtimeProfileTurnControlTraceAttrs(control) {
6563
+ if (!control) return {};
6564
+ const pendingAgeMs = Math.max(0, Date.now() - control.injectedAtMs);
6565
+ return {
6566
+ runtime_profile_control_kind: control.kind,
6567
+ runtime_profile_control_source: control.source,
6568
+ runtime_profile_key_hash: control.keyHash || void 0,
6569
+ runtime_profile_key_present: control.keyPresent,
6570
+ runtime_profile_pending_age_ms: pendingAgeMs,
6571
+ runtime_profile_requires_ack: control.kind === "migration",
6572
+ runtime_profile_migration_done_observed: control.kind === "migration" ? control.migrationDoneToolObserved : void 0
6573
+ };
6574
+ }
6575
+ activateRuntimeProfileTurnControl(ap, control) {
6576
+ ap.runtimeProfileTurnControl = control;
6577
+ }
6578
+ runtimeProfileTurnControlFromMessages(messages, source = "message") {
6579
+ const notifications = messages?.map((message) => runtimeProfileNotificationFromMessage(message)).filter((candidate) => Boolean(candidate)) ?? [];
6580
+ const notification = notifications.find((candidate) => candidate.kind === "migration") ?? notifications[0];
6581
+ if (!notification) return null;
6582
+ return runtimeProfileTurnControl(notification.kind, notification.key, source);
6583
+ }
6584
+ noteRuntimeProfileToolCall(agentId, ap, toolName) {
6585
+ const control = ap.runtimeProfileTurnControl;
6586
+ if (!control || control.kind !== "migration") return;
6587
+ if (!toolName.includes("runtime_profile_migration_done")) return;
6588
+ control.migrationDoneToolObserved = true;
6589
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime_profile.migration_done_tool.observed", {
6590
+ ...this.runtimeProfileTurnControlTraceAttrs(control),
6591
+ tool: toolName
6592
+ });
6593
+ }
6594
+ finalizeRuntimeProfileTurnControl(agentId, ap, terminal) {
6595
+ const control = ap.runtimeProfileTurnControl;
6596
+ if (!control) return {};
6597
+ const attrs = this.runtimeProfileTurnControlTraceAttrs(control);
6598
+ if (control.kind === "migration" && !control.migrationDoneToolObserved) {
6599
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime_profile.migration.turn_without_ack", {
6600
+ ...attrs,
6601
+ terminal,
6602
+ inbox_count: ap.inbox.length,
6603
+ pending_notification_count: ap.pendingNotificationCount
6604
+ });
6605
+ }
6606
+ ap.runtimeProfileTurnControl = null;
6607
+ return {
6608
+ ...attrs,
6609
+ runtime_profile_turn_terminal: terminal,
6610
+ runtime_profile_turn_outcome: control.kind === "migration" ? control.migrationDoneToolObserved ? "migration_done_observed" : "missing_migration_done" : "notice_only"
6611
+ };
6612
+ }
5917
6613
  startRuntimeTrace(agentId, ap, reason, messages) {
5918
6614
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
6615
+ const messageControl = this.runtimeProfileTurnControlFromMessages(messages);
6616
+ if (messageControl) {
6617
+ this.activateRuntimeProfileTurnControl(ap, messageControl);
6618
+ }
5919
6619
  const span = this.tracer.startSpan("daemon.runtime.turn", {
5920
6620
  surface: "daemon",
5921
6621
  kind: "internal",
@@ -5925,10 +6625,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5925
6625
  model: ap.config.model,
5926
6626
  reason,
5927
6627
  hasSession: Boolean(ap.sessionId),
5928
- ...this.messagesTraceAttrs(messages)
6628
+ ...this.messagesTraceAttrs(messages),
6629
+ ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
5929
6630
  }
5930
6631
  });
5931
- span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
6632
+ span.addEvent("daemon.turn.started", {
6633
+ reason,
6634
+ ...this.messagesTraceAttrs(messages),
6635
+ ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
6636
+ });
5932
6637
  ap.runtimeTraceSpan = span;
5933
6638
  return span;
5934
6639
  }
@@ -6034,7 +6739,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6034
6739
  ageMs: staleForMs,
6035
6740
  lastActivity: ap.lastActivity,
6036
6741
  lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6037
- lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
6742
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6743
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6038
6744
  });
6039
6745
  this.broadcastActivity(agentId, "error", diagnostic.detail);
6040
6746
  return true;
@@ -6065,7 +6771,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6065
6771
  lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6066
6772
  lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6067
6773
  pendingMessages: ap.inbox.length,
6068
- recovery: "terminate_for_queued_message"
6774
+ recovery: "terminate_for_queued_message",
6775
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6069
6776
  });
6070
6777
  ap.expectedTerminationReason = "stalled_recovery";
6071
6778
  const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
@@ -6074,6 +6781,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6074
6781
  );
6075
6782
  this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
6076
6783
  try {
6784
+ this.processExitTraceAttrs.set(ap.process, {
6785
+ stop_source: "stalled_recovery",
6786
+ expectedTerminationReason: "stalled_recovery",
6787
+ queued_messages_count: ap.inbox.length
6788
+ });
6077
6789
  ap.process.kill("SIGTERM");
6078
6790
  } catch (err) {
6079
6791
  const reason = err instanceof Error ? err.message : String(err);
@@ -6122,6 +6834,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6122
6834
  if (ap) {
6123
6835
  ap.gatedSteering.outstandingToolUses++;
6124
6836
  this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
6837
+ this.noteRuntimeProfileToolCall(agentId, ap, invocation.toolName);
6125
6838
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
6126
6839
  this.setGatedSteeringPhase(agentId, ap, "tool_wait", {
6127
6840
  event: "tool_call",
@@ -6211,11 +6924,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6211
6924
  this.broadcastActivity(agentId, "online", "Idle");
6212
6925
  }
6213
6926
  }
6214
- this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
6927
+ this.endRuntimeTrace(ap, "ok", {
6928
+ outcome: "turn-completed",
6929
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "turn_end")
6930
+ });
6215
6931
  if (ap.driver.terminateProcessOnTurnEnd) {
6216
6932
  ap.expectedTerminationReason = "turn_end";
6217
6933
  logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
6218
6934
  try {
6935
+ this.processExitTraceAttrs.set(ap.process, {
6936
+ stop_source: "turn_end",
6937
+ expectedTerminationReason: "turn_end"
6938
+ });
6219
6939
  ap.process.kill("SIGTERM");
6220
6940
  } catch (err) {
6221
6941
  const reason = err instanceof Error ? err.message : String(err);
@@ -6233,6 +6953,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6233
6953
  this.flushPendingTrajectory(agentId);
6234
6954
  if (ap) ap.lastRuntimeError = event.message;
6235
6955
  if (ap) {
6956
+ const runtimeErrorDiagnostics = buildRuntimeErrorDiagnosticEnvelope(event.message);
6236
6957
  this.setGatedSteeringPhase(agentId, ap, "error", { event: "error" });
6237
6958
  if (ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message)) {
6238
6959
  this.requeueGatedInFlightBatch(agentId, ap, "thinking_block_mutation_error");
@@ -6246,8 +6967,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6246
6967
  `[Agent ${agentId}] Disabled Claude tool-boundary gated steering after thinking-block mutation error; lastFlushReason=${ap.gatedSteering.lastFlushReason || "none"}`
6247
6968
  );
6248
6969
  }
6249
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
6250
- this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6970
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", runtimeErrorDiagnostics.eventAttrs);
6971
+ this.endRuntimeTrace(ap, "error", {
6972
+ outcome: "runtime-error",
6973
+ ...runtimeErrorDiagnostics.spanAttrs,
6974
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
6975
+ });
6251
6976
  if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6252
6977
  ap.isIdle = true;
6253
6978
  ap.pendingNotificationCount = 0;
@@ -6418,8 +7143,8 @@ ${RESPONSE_TARGET_HINT}`);
6418
7143
  const nodes = [];
6419
7144
  for (const entry of entries) {
6420
7145
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
6421
- const fullPath = path11.join(dir, entry.name);
6422
- const relativePath = path11.relative(rootDir, fullPath);
7146
+ const fullPath = path12.join(dir, entry.name);
7147
+ const relativePath = path12.relative(rootDir, fullPath);
6423
7148
  let info;
6424
7149
  try {
6425
7150
  info = await stat2(fullPath);
@@ -6721,11 +7446,10 @@ var ReminderCache = class {
6721
7446
  };
6722
7447
 
6723
7448
  // src/machineLock.ts
6724
- import { createHash, randomUUID as randomUUID2 } from "crypto";
6725
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
7449
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
7450
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
6726
7451
  import os6 from "os";
6727
- import path12 from "path";
6728
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
7452
+ import path13 from "path";
6729
7453
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
6730
7454
  var DaemonMachineLockConflictError = class extends Error {
6731
7455
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -6738,13 +7462,16 @@ var DaemonMachineLockConflictError = class extends Error {
6738
7462
  }
6739
7463
  };
6740
7464
  function apiKeyFingerprint(apiKey) {
6741
- return createHash("sha256").update(apiKey).digest("hex");
7465
+ return createHash3("sha256").update(apiKey).digest("hex");
6742
7466
  }
6743
7467
  function getDaemonMachineLockId(apiKey) {
6744
7468
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
6745
7469
  }
7470
+ function resolveDefaultMachineStateRoot() {
7471
+ return resolveSlockHomePath("machines");
7472
+ }
6746
7473
  function ownerPath(lockDir) {
6747
- return path12.join(lockDir, "owner.json");
7474
+ return path13.join(lockDir, "owner.json");
6748
7475
  }
6749
7476
  function readOwner(lockDir) {
6750
7477
  try {
@@ -6771,16 +7498,16 @@ function isProcessAlive(pid) {
6771
7498
  }
6772
7499
  }
6773
7500
  function acquireDaemonMachineLock(options) {
6774
- const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
7501
+ const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
6775
7502
  const fingerprint = apiKeyFingerprint(options.apiKey);
6776
7503
  const lockId = getDaemonMachineLockId(options.apiKey);
6777
- const machineDir = path12.join(rootDir, lockId);
6778
- const lockDir = path12.join(machineDir, "daemon.lock");
7504
+ const machineDir = path13.join(rootDir, lockId);
7505
+ const lockDir = path13.join(machineDir, "daemon.lock");
6779
7506
  const token = randomUUID2();
6780
- mkdirSync5(machineDir, { recursive: true });
7507
+ mkdirSync6(machineDir, { recursive: true });
6781
7508
  for (let attempt = 0; attempt < 2; attempt += 1) {
6782
7509
  try {
6783
- mkdirSync5(lockDir);
7510
+ mkdirSync6(lockDir);
6784
7511
  const owner = {
6785
7512
  pid: process.pid,
6786
7513
  token,
@@ -6790,7 +7517,7 @@ function acquireDaemonMachineLock(options) {
6790
7517
  apiKeyFingerprint: fingerprint.slice(0, 16)
6791
7518
  };
6792
7519
  try {
6793
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
7520
+ writeFileSync9(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
6794
7521
  `, { mode: 384 });
6795
7522
  } catch (err) {
6796
7523
  rmSync2(lockDir, { recursive: true, force: true });
@@ -6827,8 +7554,8 @@ function acquireDaemonMachineLock(options) {
6827
7554
  }
6828
7555
 
6829
7556
  // src/localTraceSink.ts
6830
- import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6831
- import path13 from "path";
7557
+ import { appendFileSync, mkdirSync as mkdirSync7, readdirSync as readdirSync4, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
7558
+ import path14 from "path";
6832
7559
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6833
7560
  var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
6834
7561
  var DEFAULT_MAX_FILES = 8;
@@ -6844,6 +7571,15 @@ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6844
7571
  "deliveryCorrelationId",
6845
7572
  "delivery_correlation_id"
6846
7573
  ]);
7574
+ var DIAGNOSTIC_ERROR_ATTRS = /* @__PURE__ */ new Set([
7575
+ "runtime_error_class",
7576
+ "runtime_error_fingerprint",
7577
+ "runtime_error_http_status",
7578
+ "runtime_error_message_present",
7579
+ "runtime_error_message_length_bucket",
7580
+ "runtime_error_message_truncated",
7581
+ "runtime_error_message_excerpt"
7582
+ ]);
6847
7583
  var LocalRotatingTraceSink = class {
6848
7584
  traceDir;
6849
7585
  maxFileBytes;
@@ -6855,7 +7591,7 @@ var LocalRotatingTraceSink = class {
6855
7591
  currentSize = 0;
6856
7592
  sequence = 0;
6857
7593
  constructor(options) {
6858
- this.traceDir = path13.join(options.machineDir, "traces");
7594
+ this.traceDir = path14.join(options.machineDir, "traces");
6859
7595
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6860
7596
  const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
6861
7597
  const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
@@ -6881,26 +7617,26 @@ var LocalRotatingTraceSink = class {
6881
7617
  return this.currentFile;
6882
7618
  }
6883
7619
  ensureFile(nextBytes) {
6884
- mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
7620
+ mkdirSync7(this.traceDir, { recursive: true, mode: 448 });
6885
7621
  const nowMs = this.nowMsProvider();
6886
7622
  const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
6887
7623
  if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
6888
- this.currentFile = path13.join(
7624
+ this.currentFile = path14.join(
6889
7625
  this.traceDir,
6890
7626
  `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6891
7627
  );
6892
- writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
7628
+ writeFileSync10(this.currentFile, "", { flag: "a", mode: 384 });
6893
7629
  this.currentSize = statSync4(this.currentFile).size;
6894
7630
  this.currentFileOpenedAtMs = nowMs;
6895
7631
  this.pruneOldFiles();
6896
7632
  }
6897
7633
  }
6898
7634
  pruneOldFiles() {
6899
- const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
7635
+ const files = readdirSync4(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
6900
7636
  const excess = files.length - this.maxFiles;
6901
7637
  if (excess <= 0) return;
6902
7638
  for (const file of files.slice(0, excess)) {
6903
- rmSync3(path13.join(this.traceDir, file), { force: true });
7639
+ rmSync3(path14.join(this.traceDir, file), { force: true });
6904
7640
  }
6905
7641
  }
6906
7642
  };
@@ -6941,6 +7677,11 @@ function sanitizeAttrs(attrs) {
6941
7677
  sanitized[key] = sanitizeValue(value);
6942
7678
  continue;
6943
7679
  }
7680
+ if (isDiagnosticErrorAttr(key)) {
7681
+ if (value === null || value === void 0 || value === "") continue;
7682
+ sanitized[key] = sanitizeValue(value);
7683
+ continue;
7684
+ }
6944
7685
  if (shouldDropAttr(key)) continue;
6945
7686
  sanitized[key] = sanitizeValue(value);
6946
7687
  }
@@ -6974,16 +7715,19 @@ function shouldDropAttr(key) {
6974
7715
  function isDiagnosticIdAttr(key) {
6975
7716
  return DIAGNOSTIC_ID_ATTRS.has(key);
6976
7717
  }
7718
+ function isDiagnosticErrorAttr(key) {
7719
+ return DIAGNOSTIC_ERROR_ATTRS.has(key);
7720
+ }
6977
7721
 
6978
7722
  // src/traceBundleUpload.ts
6979
- import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
7723
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
6980
7724
  import { gzipSync } from "zlib";
6981
7725
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
6982
- import path14 from "path";
7726
+ import path15 from "path";
6983
7727
 
6984
7728
  // src/directUploadCapability.ts
6985
- function joinUrl(base, path16) {
6986
- return `${base.replace(/\/+$/, "")}${path16}`;
7729
+ function joinUrl(base, path17) {
7730
+ return `${base.replace(/\/+$/, "")}${path17}`;
6987
7731
  }
6988
7732
  function jsonHeaders(apiKey) {
6989
7733
  return {
@@ -7106,12 +7850,12 @@ async function uploadWithSignedCapability({
7106
7850
  }
7107
7851
 
7108
7852
  // src/traceJitter.ts
7109
- import { createHash as createHash2 } from "crypto";
7853
+ import { createHash as createHash4 } from "crypto";
7110
7854
  var INITIAL_UPLOAD_DELAY_SPAN_MS = 3e4;
7111
7855
  var UPLOAD_INTERVAL_JITTER_SPAN_MS = 6e4;
7112
7856
  var MAX_FILE_AGE_JITTER_SPAN_MS = 6e4;
7113
7857
  function computeTraceJitter(lockId) {
7114
- const seed = createHash2("sha256").update(lockId).digest();
7858
+ const seed = createHash4("sha256").update(lockId).digest();
7115
7859
  return {
7116
7860
  initialUploadDelayMs: seed.readUInt32BE(0) % INITIAL_UPLOAD_DELAY_SPAN_MS,
7117
7861
  uploadIntervalJitterMs: seed.readUInt32BE(4) % UPLOAD_INTERVAL_JITTER_SPAN_MS,
@@ -7202,7 +7946,7 @@ var DaemonTraceBundleUploader = class {
7202
7946
  }, nextMs);
7203
7947
  }
7204
7948
  async findUploadCandidates() {
7205
- const traceDir = path14.join(this.options.machineDir, "traces");
7949
+ const traceDir = path15.join(this.options.machineDir, "traces");
7206
7950
  let names;
7207
7951
  try {
7208
7952
  names = await readdir3(traceDir);
@@ -7214,8 +7958,8 @@ var DaemonTraceBundleUploader = class {
7214
7958
  const currentFile = this.options.currentFileProvider?.();
7215
7959
  const candidates = [];
7216
7960
  for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
7217
- const file = path14.join(traceDir, name);
7218
- if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
7961
+ const file = path15.join(traceDir, name);
7962
+ if (currentFile && path15.resolve(file) === path15.resolve(currentFile)) continue;
7219
7963
  if (await this.isUploaded(file)) continue;
7220
7964
  try {
7221
7965
  const info = await stat3(file);
@@ -7289,8 +8033,8 @@ var DaemonTraceBundleUploader = class {
7289
8033
  }
7290
8034
  }
7291
8035
  uploadStatePath(file) {
7292
- const stateDir = path14.join(this.options.machineDir, "trace-uploads");
7293
- return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
8036
+ const stateDir = path15.join(this.options.machineDir, "trace-uploads");
8037
+ return path15.join(stateDir, `${path15.basename(file)}.uploaded.json`);
7294
8038
  }
7295
8039
  async isUploaded(file) {
7296
8040
  try {
@@ -7302,9 +8046,9 @@ var DaemonTraceBundleUploader = class {
7302
8046
  }
7303
8047
  async markUploaded(file, metadata) {
7304
8048
  const stateFile = this.uploadStatePath(file);
7305
- await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
8049
+ await mkdir2(path15.dirname(stateFile), { recursive: true, mode: 448 });
7306
8050
  await writeFile2(stateFile, `${JSON.stringify({
7307
- file: path14.basename(file),
8051
+ file: path15.basename(file),
7308
8052
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
7309
8053
  ...metadata
7310
8054
  }, null, 2)}
@@ -7312,7 +8056,7 @@ var DaemonTraceBundleUploader = class {
7312
8056
  }
7313
8057
  };
7314
8058
  function sha256Hex(body) {
7315
- return createHash3("sha256").update(body).digest("hex");
8059
+ return createHash5("sha256").update(body).digest("hex");
7316
8060
  }
7317
8061
  function readPositiveIntegerEnv2(name, fallback) {
7318
8062
  const value = process.env[name];
@@ -7343,23 +8087,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
7343
8087
  }
7344
8088
  }
7345
8089
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
7346
- const dirname = path15.dirname(fileURLToPath(moduleUrl));
7347
- const jsPath = path15.resolve(dirname, "chat-bridge.js");
8090
+ const dirname = path16.dirname(fileURLToPath2(moduleUrl));
8091
+ const jsPath = path16.resolve(dirname, "chat-bridge.js");
7348
8092
  try {
7349
8093
  accessSync(jsPath);
7350
8094
  return jsPath;
7351
8095
  } catch {
7352
- return path15.resolve(dirname, "chat-bridge.ts");
8096
+ return path16.resolve(dirname, "chat-bridge.ts");
7353
8097
  }
7354
8098
  }
7355
8099
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
7356
- const thisDir = path15.dirname(fileURLToPath(moduleUrl));
7357
- const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
8100
+ const thisDir = path16.dirname(fileURLToPath2(moduleUrl));
8101
+ const bundledDistPath = path16.resolve(thisDir, "cli", "index.js");
7358
8102
  try {
7359
8103
  accessSync(bundledDistPath);
7360
8104
  return bundledDistPath;
7361
8105
  } catch {
7362
- const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
8106
+ const workspaceDistPath = path16.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
7363
8107
  accessSync(workspaceDistPath);
7364
8108
  return workspaceDistPath;
7365
8109
  }
@@ -7491,6 +8235,7 @@ var DaemonCore = class {
7491
8235
  daemonVersion;
7492
8236
  chatBridgePath;
7493
8237
  slockCliPath;
8238
+ slockHome;
7494
8239
  runtimeDetector;
7495
8240
  agentManager;
7496
8241
  connection;
@@ -7505,6 +8250,8 @@ var DaemonCore = class {
7505
8250
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
7506
8251
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
7507
8252
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
8253
+ this.slockHome = resolveSlockHome();
8254
+ process.env[SLOCK_HOME_ENV] = this.slockHome;
7508
8255
  this.injectedTracer = Boolean(options.tracer);
7509
8256
  this.tracer = options.tracer ?? noopTracer;
7510
8257
  this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
@@ -7514,7 +8261,7 @@ var DaemonCore = class {
7514
8261
  });
7515
8262
  let connection;
7516
8263
  const agentManagerOptions = {
7517
- dataDir: options.dataDir,
8264
+ dataDir: options.dataDir ?? resolveSlockHomePath("agents", this.slockHome),
7518
8265
  serverUrl: options.serverUrl,
7519
8266
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
7520
8267
  slockCliPath: this.slockCliPath,
@@ -7535,8 +8282,8 @@ var DaemonCore = class {
7535
8282
  }
7536
8283
  resolveMachineStateRoot() {
7537
8284
  if (this.options.machineStateDir) return this.options.machineStateDir;
7538
- if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
7539
- return DEFAULT_MACHINE_STATE_ROOT;
8285
+ if (this.options.dataDir) return path16.join(path16.dirname(this.options.dataDir), "machines");
8286
+ return resolveDefaultMachineStateRoot();
7540
8287
  }
7541
8288
  shouldEnableLocalTrace() {
7542
8289
  if (this.injectedTracer) return false;
@@ -7580,6 +8327,12 @@ var DaemonCore = class {
7580
8327
  }
7581
8328
  start() {
7582
8329
  logger.info("[Slock Daemon] Starting...");
8330
+ logger.info(`[Slock Daemon] ${SLOCK_HOME_ENV}=${this.slockHome}`);
8331
+ for (const legacy of listLegacySlockStatePaths(this.slockHome)) {
8332
+ logger.warn(
8333
+ `[Slock Daemon] Legacy Slock state exists outside ${SLOCK_HOME_ENV}: ${legacy.path}. This daemon will use ${legacy.destination}; migrate manually if that ${legacy.description} should move with this installation.`
8334
+ );
8335
+ }
7583
8336
  if (!this.machineLock) {
7584
8337
  this.machineLock = acquireDaemonMachineLock({
7585
8338
  apiKey: this.options.apiKey,