@slock-ai/daemon 0.46.2 → 0.47.0-staging.20260511151721

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,10 +1,14 @@
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
14
  import path15 from "path";
@@ -576,6 +580,66 @@ 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
+
579
643
  // ../shared/src/testing/failpoints.ts
580
644
  var NoopFailpointRegistry = class {
581
645
  get enabled() {
@@ -688,6 +752,7 @@ var DISPLAY_PLAN_CONFIG = {
688
752
  // src/agentProcessManager.ts
689
753
  import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
690
754
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
755
+ import { createHash as createHash2 } from "crypto";
691
756
  import path11 from "path";
692
757
  import os5 from "os";
693
758
 
@@ -796,6 +861,7 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
796
861
  21. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
797
862
  22. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
798
863
  23. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
864
+ 24. **\`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
865
 
800
866
  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
867
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -1272,6 +1338,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
1272
1338
  ...ctx.config.envVars || {},
1273
1339
  ...extraEnv,
1274
1340
  ...runtimeContextEnv(ctx.config),
1341
+ [SLOCK_HOME_ENV]: resolveSlockHome(),
1275
1342
  SLOCK_AGENT_ID: ctx.agentId,
1276
1343
  ...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
1277
1344
  SLOCK_SERVER_URL: ctx.config.serverUrl,
@@ -2228,6 +2295,15 @@ function detectCodexModels(home = os2.homedir()) {
2228
2295
  import { spawn as spawn3 } from "child_process";
2229
2296
  import path5 from "path";
2230
2297
  import { writeFileSync as writeFileSync3 } from "fs";
2298
+ function buildCopilotSpawnEnv(ctx) {
2299
+ return {
2300
+ ...process.env,
2301
+ FORCE_COLOR: "0",
2302
+ NO_COLOR: "1",
2303
+ ...ctx.config.envVars || {},
2304
+ [SLOCK_HOME_ENV]: resolveSlockHome()
2305
+ };
2306
+ }
2231
2307
  var CopilotDriver = class {
2232
2308
  id = "copilot";
2233
2309
  lifecycle = {
@@ -2286,7 +2362,7 @@ var CopilotDriver = class {
2286
2362
  if (ctx.config.sessionId) {
2287
2363
  args.push(`--resume=${ctx.config.sessionId}`);
2288
2364
  }
2289
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2365
+ const spawnEnv = buildCopilotSpawnEnv(ctx);
2290
2366
  const proc = spawn3("copilot", args, {
2291
2367
  cwd: ctx.workingDirectory,
2292
2368
  stdio: ["pipe", "pipe", "pipe"],
@@ -2383,6 +2459,15 @@ var CopilotDriver = class {
2383
2459
  import { spawn as spawn4, spawnSync } from "child_process";
2384
2460
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
2385
2461
  import path6 from "path";
2462
+ function buildCursorSpawnEnv(ctx) {
2463
+ return {
2464
+ ...process.env,
2465
+ FORCE_COLOR: "0",
2466
+ NO_COLOR: "1",
2467
+ ...ctx.config.envVars || {},
2468
+ [SLOCK_HOME_ENV]: resolveSlockHome()
2469
+ };
2470
+ }
2386
2471
  var CursorDriver = class {
2387
2472
  id = "cursor";
2388
2473
  lifecycle = {
@@ -2437,7 +2522,7 @@ var CursorDriver = class {
2437
2522
  args.push("--resume", ctx.config.sessionId);
2438
2523
  }
2439
2524
  args.push(ctx.prompt);
2440
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2525
+ const spawnEnv = buildCursorSpawnEnv(ctx);
2441
2526
  const proc = spawn4("cursor-agent", args, {
2442
2527
  cwd: ctx.workingDirectory,
2443
2528
  stdio: ["pipe", "pipe", "pipe"],
@@ -3527,8 +3612,86 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
3527
3612
  }
3528
3613
  }
3529
3614
 
3615
+ // src/runtimeErrorDiagnostics.ts
3616
+ import { createHash } from "crypto";
3617
+ var MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS = 4096;
3618
+ function buildRuntimeErrorDiagnosticEnvelope(message) {
3619
+ const rawMessage = String(message || "");
3620
+ const scrubbed = scrubRuntimeErrorDiagnosticText(rawMessage);
3621
+ const { value: excerpt, truncated } = truncateDiagnosticText(scrubbed, MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS);
3622
+ const httpStatus = extractHttpStatus(rawMessage);
3623
+ const runtimeErrorClass = classifyRuntimeError(rawMessage, httpStatus);
3624
+ const fingerprint = fingerprintRuntimeError(scrubbed);
3625
+ const spanAttrs = {
3626
+ runtime_error_class: runtimeErrorClass,
3627
+ runtime_error_fingerprint: fingerprint,
3628
+ runtime_error_message_present: rawMessage.length > 0,
3629
+ runtime_error_message_length_bucket: bucketLength(rawMessage.length),
3630
+ runtime_error_message_truncated: truncated
3631
+ };
3632
+ if (httpStatus !== null) {
3633
+ spanAttrs.runtime_error_http_status = httpStatus;
3634
+ }
3635
+ return {
3636
+ spanAttrs,
3637
+ eventAttrs: {
3638
+ ...spanAttrs,
3639
+ runtime_error_message_excerpt: excerpt
3640
+ }
3641
+ };
3642
+ }
3643
+ function scrubRuntimeErrorDiagnosticText(value) {
3644
+ 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]");
3645
+ }
3646
+ function truncateDiagnosticText(value, maxChars) {
3647
+ if (value.length <= maxChars) return { value, truncated: false };
3648
+ return { value: value.slice(0, maxChars), truncated: true };
3649
+ }
3650
+ function extractHttpStatus(message) {
3651
+ 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);
3652
+ if (!match) return null;
3653
+ const status = Number(match[1]);
3654
+ return Number.isInteger(status) ? status : null;
3655
+ }
3656
+ function classifyRuntimeError(message, httpStatus) {
3657
+ const explicit = /\b([A-Z][A-Za-z0-9_]*(?:Error|Exception))\b/.exec(message);
3658
+ if (explicit) return explicit[1];
3659
+ if (httpStatus !== null) {
3660
+ if (httpStatus === 429) return "RateLimitError";
3661
+ if (httpStatus === 401 || httpStatus === 403) return "AuthError";
3662
+ if (httpStatus === 404) return "NotFoundError";
3663
+ if (httpStatus >= 500) return "ProviderServerError";
3664
+ return "ProviderApiError";
3665
+ }
3666
+ if (/\btimeout|timed out\b/i.test(message)) return "TimeoutError";
3667
+ if (/\brate.?limit|too many requests\b/i.test(message)) return "RateLimitError";
3668
+ if (/\bnot found\b/i.test(message)) return "NotFoundError";
3669
+ return "RuntimeError";
3670
+ }
3671
+ function fingerprintRuntimeError(value) {
3672
+ const normalized = value.toLowerCase().replace(/[0-9a-f]{12,}/g, "<hex>").replace(/\b\d+\b/g, "<num>").replace(/\s+/g, " ").trim();
3673
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
3674
+ }
3675
+ function bucketLength(length) {
3676
+ if (length === 0) return "0";
3677
+ if (length < 1024) return "<1k";
3678
+ if (length < 4096) return "1k-4k";
3679
+ if (length < 16384) return "4k-16k";
3680
+ return "16k+";
3681
+ }
3682
+ function redactUrlQuery(value) {
3683
+ try {
3684
+ const url = new URL(value);
3685
+ if (url.search) url.search = "?[REDACTED_QUERY]";
3686
+ if (url.username) url.username = "[REDACTED_USER]";
3687
+ if (url.password) url.password = "[REDACTED_PASSWORD]";
3688
+ return url.toString();
3689
+ } catch {
3690
+ return value.replace(/\?.*$/, "?[REDACTED_QUERY]");
3691
+ }
3692
+ }
3693
+
3530
3694
  // src/agentProcessManager.ts
3531
- var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
3532
3695
  var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
3533
3696
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
3534
3697
  var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
@@ -3857,15 +4020,27 @@ After confirming language preference, do not give a generic product introduction
3857
4020
  - Use a low-pressure prompt and guide to one concrete starter action.
3858
4021
 
3859
4022
  ### 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
4023
+ A starter plan should make the next action executable, not just descriptive.
4024
+ For new channels, new agents, and adding members to an existing channel, post an **action card** rather than a copyable spec:
4025
+
4026
+ - 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.
4027
+ - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
4028
+ - \`{type: "agent:create", name, description?, draftHint?}\`
4029
+ - \`{type: "channel:add_member", channel: "#existing-channel", humans?: ["@alice"], agents?: ["@scout"], draftHint?}\` \u2014 at least one of humans / agents must be non-empty
4030
+ - 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.
4031
+ - 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).
4032
+ - 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.
4033
+ - 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".
4034
+ - Do not imply the resource has been created or members added until the card flips to "Done".
4035
+
4036
+ Other plan elements still apply:
4037
+ - suggested channel or workstream pairing
4038
+ - first task to send after the card is committed
4039
+ - who to pull in once the channel exists (often a follow-up \`channel:add_member\` card)
3865
4040
 
3866
4041
  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.
4042
+ If details are missing but not blocking, state reasonable defaults inside the action card payload (the owner can edit) and invite correction in chat.
4043
+ Only ask one blocking question first if the answer is required before any useful card can be prepared.
3869
4044
  Do not imply you have already created agents or channels unless the action has actually happened.
3870
4045
 
3871
4046
  ### Capability Boundary Pivot
@@ -4103,16 +4278,22 @@ Do not copy these answers verbatim.
4103
4278
 
4104
4279
  ## FAQ 15: How do I create agents or channels?
4105
4280
  ### 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.
4281
+ - 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.
4282
+ - v1 supports three action types via \`slock action prepare --target '<channel>' <<EOF { ... } EOF\`:
4283
+ - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
4284
+ - \`{type: "agent:create", name, description?, draftHint?}\` \u2014 runtime / model / computer are the owner's call, not yours
4285
+ - \`{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.
4286
+
4287
+ - **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.
4288
+ - 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
4289
 
4110
4290
  ### Next step
4111
- - Give the exact agent/channel spec the user can copy, then guide them to the relevant + button or creation dialog.
4291
+ - 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
4292
 
4113
4293
  ### 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.
4294
+ - Do not imply you already created agents or channels unless the card state is \`executed\`.
4295
+ - Do not prefill technical fields on \`agent:create\` (runtime / model / computer / reasoning effort). The owner picks those in the dialog.
4296
+ - 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
4297
  `;
4117
4298
  }
4118
4299
  function buildOnboardingSeedFiles() {
@@ -4212,6 +4393,20 @@ function runtimeProfileNotificationFromMessage(message) {
4212
4393
  function runtimeProfileNotificationTitle(kind) {
4213
4394
  return kind === "migration" ? "Runtime Profile migration" : "Runtime Profile notice";
4214
4395
  }
4396
+ function hashRuntimeProfileKey(key) {
4397
+ if (!key) return void 0;
4398
+ return createHash2("sha256").update(key).digest("hex").slice(0, 16);
4399
+ }
4400
+ function runtimeProfileTurnControl(kind, key, source) {
4401
+ return {
4402
+ kind,
4403
+ source,
4404
+ keyHash: hashRuntimeProfileKey(key) ?? null,
4405
+ keyPresent: Boolean(key),
4406
+ injectedAtMs: Date.now(),
4407
+ migrationDoneToolObserved: false
4408
+ };
4409
+ }
4215
4410
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
4216
4411
  const next = [...lines];
4217
4412
  for (const rawLine of chunk.split(/\r?\n/)) {
@@ -4400,13 +4595,14 @@ var AgentProcessManager = class _AgentProcessManager {
4400
4595
  defaultAgentEnvVarsProvider;
4401
4596
  tracer;
4402
4597
  deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
4598
+ processExitTraceAttrs = /* @__PURE__ */ new WeakMap();
4403
4599
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
4404
4600
  this.chatBridgePath = chatBridgePath;
4405
4601
  this.slockCliPath = opts.slockCliPath ?? "";
4406
4602
  this.sendToServer = sendToServer;
4407
4603
  this.daemonApiKey = daemonApiKey;
4408
4604
  this.serverUrl = opts.serverUrl;
4409
- this.dataDir = opts.dataDir || DATA_DIR;
4605
+ this.dataDir = opts.dataDir || resolveSlockHomePath("agents");
4410
4606
  this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
4411
4607
  this.driverResolver = opts.driverResolver || getDriver;
4412
4608
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
@@ -4798,6 +4994,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4798
4994
  exitCode: null,
4799
4995
  exitSignal: null,
4800
4996
  expectedTerminationReason: null,
4997
+ runtimeProfileTurnControl: runtimeConfig.runtimeProfileControl ? runtimeProfileTurnControl(runtimeConfig.runtimeProfileControl.kind, runtimeConfig.runtimeProfileControl.key, "agent_config") : null,
4801
4998
  pendingTrajectory: null,
4802
4999
  gatedSteering: createGatedSteeringState()
4803
5000
  };
@@ -4872,7 +5069,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4872
5069
  clean_exit: code === 0,
4873
5070
  runtime_trace_active: Boolean(current?.runtimeTraceSpan),
4874
5071
  inbox_count: current?.inbox.length ?? 0,
4875
- pending_notification_count: current?.pendingNotificationCount ?? 0
5072
+ pending_notification_count: current?.pendingNotificationCount ?? 0,
5073
+ ...this.processExitTraceAttrs.get(proc)
4876
5074
  });
4877
5075
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
4878
5076
  });
@@ -4900,7 +5098,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
4900
5098
  outcome: processEndedCleanly ? "process-exit" : "process-crash",
4901
5099
  expectedTerminationReason: ap.expectedTerminationReason || void 0,
4902
5100
  exitCode: finalCode,
4903
- exitSignal: finalSignal
5101
+ exitSignal: finalSignal,
5102
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "process_exit")
4904
5103
  });
4905
5104
  if (processEndedCleanly) {
4906
5105
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
@@ -5063,6 +5262,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5063
5262
  agentId,
5064
5263
  kind,
5065
5264
  key_present: Boolean(key),
5265
+ key_hash: hashRuntimeProfileKey(key),
5066
5266
  outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
5067
5267
  runtime: ap.config.runtime,
5068
5268
  session_id_present: Boolean(ap.sessionId),
@@ -5085,6 +5285,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5085
5285
  agentId,
5086
5286
  kind,
5087
5287
  key_present: Boolean(key),
5288
+ key_hash: hashRuntimeProfileKey(key),
5088
5289
  outcome: "queued_during_start",
5089
5290
  startup_pending: true,
5090
5291
  starting_inbox_count: pending.length,
@@ -5121,6 +5322,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5121
5322
  }
5122
5323
  this.clearCompactionWatchdog(ap);
5123
5324
  this.agents.delete(agentId);
5325
+ this.processExitTraceAttrs.set(ap.process, {
5326
+ stop_source: silent ? "daemon_internal" : "explicit_request",
5327
+ stop_wait_requested: wait,
5328
+ stop_silent: silent
5329
+ });
5124
5330
  ap.process.kill("SIGTERM");
5125
5331
  if (!silent) {
5126
5332
  this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
@@ -5411,7 +5617,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5411
5617
  attrs: {
5412
5618
  agentId,
5413
5619
  control_kind: kind,
5414
- key_present: Boolean(key)
5620
+ key_present: Boolean(key),
5621
+ key_hash: hashRuntimeProfileKey(key)
5415
5622
  }
5416
5623
  });
5417
5624
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5444,7 +5651,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5444
5651
  }
5445
5652
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
5446
5653
  ap.isIdle = false;
5447
- this.startRuntimeTrace(agentId, ap, "runtime-profile");
5654
+ this.startRuntimeTrace(agentId, ap, "runtime-profile", [message]);
5448
5655
  const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
5449
5656
  span.end(written ? "ok" : "error", {
5450
5657
  attrs: {
@@ -5539,6 +5746,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5539
5746
  agentId,
5540
5747
  control_kind: control.kind,
5541
5748
  key_present: Boolean(control.key),
5749
+ key_hash: hashRuntimeProfileKey(control.key),
5542
5750
  launchId: launchId || void 0,
5543
5751
  source: "agent_config"
5544
5752
  }
@@ -5903,7 +6111,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5903
6111
  deliveryId: context.deliveryId,
5904
6112
  delivery_correlation_id: context.deliveryId,
5905
6113
  control_kind: notification.kind,
5906
- key_present: Boolean(notification.key)
6114
+ key_present: Boolean(notification.key),
6115
+ runtime_profile_control_kind: notification.kind,
6116
+ runtime_profile_key_hash: hashRuntimeProfileKey(notification.key),
6117
+ runtime_profile_key_present: Boolean(notification.key)
5907
6118
  };
5908
6119
  }
5909
6120
  return {
@@ -5914,8 +6125,63 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5914
6125
  delivery_correlation_id: context.deliveryId ?? first.message_id
5915
6126
  };
5916
6127
  }
6128
+ runtimeProfileTurnControlTraceAttrs(control) {
6129
+ if (!control) return {};
6130
+ const pendingAgeMs = Math.max(0, Date.now() - control.injectedAtMs);
6131
+ return {
6132
+ runtime_profile_control_kind: control.kind,
6133
+ runtime_profile_control_source: control.source,
6134
+ runtime_profile_key_hash: control.keyHash || void 0,
6135
+ runtime_profile_key_present: control.keyPresent,
6136
+ runtime_profile_pending_age_ms: pendingAgeMs,
6137
+ runtime_profile_requires_ack: control.kind === "migration",
6138
+ runtime_profile_migration_done_observed: control.kind === "migration" ? control.migrationDoneToolObserved : void 0
6139
+ };
6140
+ }
6141
+ activateRuntimeProfileTurnControl(ap, control) {
6142
+ ap.runtimeProfileTurnControl = control;
6143
+ }
6144
+ runtimeProfileTurnControlFromMessages(messages, source = "message") {
6145
+ const notifications = messages?.map((message) => runtimeProfileNotificationFromMessage(message)).filter((candidate) => Boolean(candidate)) ?? [];
6146
+ const notification = notifications.find((candidate) => candidate.kind === "migration") ?? notifications[0];
6147
+ if (!notification) return null;
6148
+ return runtimeProfileTurnControl(notification.kind, notification.key, source);
6149
+ }
6150
+ noteRuntimeProfileToolCall(agentId, ap, toolName) {
6151
+ const control = ap.runtimeProfileTurnControl;
6152
+ if (!control || control.kind !== "migration") return;
6153
+ if (!toolName.includes("runtime_profile_migration_done")) return;
6154
+ control.migrationDoneToolObserved = true;
6155
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime_profile.migration_done_tool.observed", {
6156
+ ...this.runtimeProfileTurnControlTraceAttrs(control),
6157
+ tool: toolName
6158
+ });
6159
+ }
6160
+ finalizeRuntimeProfileTurnControl(agentId, ap, terminal) {
6161
+ const control = ap.runtimeProfileTurnControl;
6162
+ if (!control) return {};
6163
+ const attrs = this.runtimeProfileTurnControlTraceAttrs(control);
6164
+ if (control.kind === "migration" && !control.migrationDoneToolObserved) {
6165
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime_profile.migration.turn_without_ack", {
6166
+ ...attrs,
6167
+ terminal,
6168
+ inbox_count: ap.inbox.length,
6169
+ pending_notification_count: ap.pendingNotificationCount
6170
+ });
6171
+ }
6172
+ ap.runtimeProfileTurnControl = null;
6173
+ return {
6174
+ ...attrs,
6175
+ runtime_profile_turn_terminal: terminal,
6176
+ runtime_profile_turn_outcome: control.kind === "migration" ? control.migrationDoneToolObserved ? "migration_done_observed" : "missing_migration_done" : "notice_only"
6177
+ };
6178
+ }
5917
6179
  startRuntimeTrace(agentId, ap, reason, messages) {
5918
6180
  if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
6181
+ const messageControl = this.runtimeProfileTurnControlFromMessages(messages);
6182
+ if (messageControl) {
6183
+ this.activateRuntimeProfileTurnControl(ap, messageControl);
6184
+ }
5919
6185
  const span = this.tracer.startSpan("daemon.runtime.turn", {
5920
6186
  surface: "daemon",
5921
6187
  kind: "internal",
@@ -5925,10 +6191,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5925
6191
  model: ap.config.model,
5926
6192
  reason,
5927
6193
  hasSession: Boolean(ap.sessionId),
5928
- ...this.messagesTraceAttrs(messages)
6194
+ ...this.messagesTraceAttrs(messages),
6195
+ ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
5929
6196
  }
5930
6197
  });
5931
- span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
6198
+ span.addEvent("daemon.turn.started", {
6199
+ reason,
6200
+ ...this.messagesTraceAttrs(messages),
6201
+ ...this.runtimeProfileTurnControlTraceAttrs(ap.runtimeProfileTurnControl)
6202
+ });
5932
6203
  ap.runtimeTraceSpan = span;
5933
6204
  return span;
5934
6205
  }
@@ -6034,7 +6305,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6034
6305
  ageMs: staleForMs,
6035
6306
  lastActivity: ap.lastActivity,
6036
6307
  lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6037
- lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
6308
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6309
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6038
6310
  });
6039
6311
  this.broadcastActivity(agentId, "error", diagnostic.detail);
6040
6312
  return true;
@@ -6065,7 +6337,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6065
6337
  lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6066
6338
  lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
6067
6339
  pendingMessages: ap.inbox.length,
6068
- recovery: "terminate_for_queued_message"
6340
+ recovery: "terminate_for_queued_message",
6341
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
6069
6342
  });
6070
6343
  ap.expectedTerminationReason = "stalled_recovery";
6071
6344
  const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
@@ -6074,6 +6347,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6074
6347
  );
6075
6348
  this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
6076
6349
  try {
6350
+ this.processExitTraceAttrs.set(ap.process, {
6351
+ stop_source: "stalled_recovery",
6352
+ expectedTerminationReason: "stalled_recovery",
6353
+ queued_messages_count: ap.inbox.length
6354
+ });
6077
6355
  ap.process.kill("SIGTERM");
6078
6356
  } catch (err) {
6079
6357
  const reason = err instanceof Error ? err.message : String(err);
@@ -6122,6 +6400,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6122
6400
  if (ap) {
6123
6401
  ap.gatedSteering.outstandingToolUses++;
6124
6402
  this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
6403
+ this.noteRuntimeProfileToolCall(agentId, ap, invocation.toolName);
6125
6404
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
6126
6405
  this.setGatedSteeringPhase(agentId, ap, "tool_wait", {
6127
6406
  event: "tool_call",
@@ -6211,11 +6490,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6211
6490
  this.broadcastActivity(agentId, "online", "Idle");
6212
6491
  }
6213
6492
  }
6214
- this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
6493
+ this.endRuntimeTrace(ap, "ok", {
6494
+ outcome: "turn-completed",
6495
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "turn_end")
6496
+ });
6215
6497
  if (ap.driver.terminateProcessOnTurnEnd) {
6216
6498
  ap.expectedTerminationReason = "turn_end";
6217
6499
  logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
6218
6500
  try {
6501
+ this.processExitTraceAttrs.set(ap.process, {
6502
+ stop_source: "turn_end",
6503
+ expectedTerminationReason: "turn_end"
6504
+ });
6219
6505
  ap.process.kill("SIGTERM");
6220
6506
  } catch (err) {
6221
6507
  const reason = err instanceof Error ? err.message : String(err);
@@ -6233,6 +6519,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6233
6519
  this.flushPendingTrajectory(agentId);
6234
6520
  if (ap) ap.lastRuntimeError = event.message;
6235
6521
  if (ap) {
6522
+ const runtimeErrorDiagnostics = buildRuntimeErrorDiagnosticEnvelope(event.message);
6236
6523
  this.setGatedSteeringPhase(agentId, ap, "error", { event: "error" });
6237
6524
  if (ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message)) {
6238
6525
  this.requeueGatedInFlightBatch(agentId, ap, "thinking_block_mutation_error");
@@ -6246,8 +6533,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6246
6533
  `[Agent ${agentId}] Disabled Claude tool-boundary gated steering after thinking-block mutation error; lastFlushReason=${ap.gatedSteering.lastFlushReason || "none"}`
6247
6534
  );
6248
6535
  }
6249
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
6250
- this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
6536
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", runtimeErrorDiagnostics.eventAttrs);
6537
+ this.endRuntimeTrace(ap, "error", {
6538
+ outcome: "runtime-error",
6539
+ ...runtimeErrorDiagnostics.spanAttrs,
6540
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
6541
+ });
6251
6542
  if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
6252
6543
  ap.isIdle = true;
6253
6544
  ap.pendingNotificationCount = 0;
@@ -6721,11 +7012,10 @@ var ReminderCache = class {
6721
7012
  };
6722
7013
 
6723
7014
  // src/machineLock.ts
6724
- import { createHash, randomUUID as randomUUID2 } from "crypto";
7015
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
6725
7016
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
6726
7017
  import os6 from "os";
6727
7018
  import path12 from "path";
6728
- var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
6729
7019
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
6730
7020
  var DaemonMachineLockConflictError = class extends Error {
6731
7021
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -6738,11 +7028,14 @@ var DaemonMachineLockConflictError = class extends Error {
6738
7028
  }
6739
7029
  };
6740
7030
  function apiKeyFingerprint(apiKey) {
6741
- return createHash("sha256").update(apiKey).digest("hex");
7031
+ return createHash3("sha256").update(apiKey).digest("hex");
6742
7032
  }
6743
7033
  function getDaemonMachineLockId(apiKey) {
6744
7034
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
6745
7035
  }
7036
+ function resolveDefaultMachineStateRoot() {
7037
+ return resolveSlockHomePath("machines");
7038
+ }
6746
7039
  function ownerPath(lockDir) {
6747
7040
  return path12.join(lockDir, "owner.json");
6748
7041
  }
@@ -6771,7 +7064,7 @@ function isProcessAlive(pid) {
6771
7064
  }
6772
7065
  }
6773
7066
  function acquireDaemonMachineLock(options) {
6774
- const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
7067
+ const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
6775
7068
  const fingerprint = apiKeyFingerprint(options.apiKey);
6776
7069
  const lockId = getDaemonMachineLockId(options.apiKey);
6777
7070
  const machineDir = path12.join(rootDir, lockId);
@@ -6844,6 +7137,15 @@ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6844
7137
  "deliveryCorrelationId",
6845
7138
  "delivery_correlation_id"
6846
7139
  ]);
7140
+ var DIAGNOSTIC_ERROR_ATTRS = /* @__PURE__ */ new Set([
7141
+ "runtime_error_class",
7142
+ "runtime_error_fingerprint",
7143
+ "runtime_error_http_status",
7144
+ "runtime_error_message_present",
7145
+ "runtime_error_message_length_bucket",
7146
+ "runtime_error_message_truncated",
7147
+ "runtime_error_message_excerpt"
7148
+ ]);
6847
7149
  var LocalRotatingTraceSink = class {
6848
7150
  traceDir;
6849
7151
  maxFileBytes;
@@ -6941,6 +7243,11 @@ function sanitizeAttrs(attrs) {
6941
7243
  sanitized[key] = sanitizeValue(value);
6942
7244
  continue;
6943
7245
  }
7246
+ if (isDiagnosticErrorAttr(key)) {
7247
+ if (value === null || value === void 0 || value === "") continue;
7248
+ sanitized[key] = sanitizeValue(value);
7249
+ continue;
7250
+ }
6944
7251
  if (shouldDropAttr(key)) continue;
6945
7252
  sanitized[key] = sanitizeValue(value);
6946
7253
  }
@@ -6974,9 +7281,12 @@ function shouldDropAttr(key) {
6974
7281
  function isDiagnosticIdAttr(key) {
6975
7282
  return DIAGNOSTIC_ID_ATTRS.has(key);
6976
7283
  }
7284
+ function isDiagnosticErrorAttr(key) {
7285
+ return DIAGNOSTIC_ERROR_ATTRS.has(key);
7286
+ }
6977
7287
 
6978
7288
  // src/traceBundleUpload.ts
6979
- import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
7289
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
6980
7290
  import { gzipSync } from "zlib";
6981
7291
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
6982
7292
  import path14 from "path";
@@ -7106,12 +7416,12 @@ async function uploadWithSignedCapability({
7106
7416
  }
7107
7417
 
7108
7418
  // src/traceJitter.ts
7109
- import { createHash as createHash2 } from "crypto";
7419
+ import { createHash as createHash4 } from "crypto";
7110
7420
  var INITIAL_UPLOAD_DELAY_SPAN_MS = 3e4;
7111
7421
  var UPLOAD_INTERVAL_JITTER_SPAN_MS = 6e4;
7112
7422
  var MAX_FILE_AGE_JITTER_SPAN_MS = 6e4;
7113
7423
  function computeTraceJitter(lockId) {
7114
- const seed = createHash2("sha256").update(lockId).digest();
7424
+ const seed = createHash4("sha256").update(lockId).digest();
7115
7425
  return {
7116
7426
  initialUploadDelayMs: seed.readUInt32BE(0) % INITIAL_UPLOAD_DELAY_SPAN_MS,
7117
7427
  uploadIntervalJitterMs: seed.readUInt32BE(4) % UPLOAD_INTERVAL_JITTER_SPAN_MS,
@@ -7312,7 +7622,7 @@ var DaemonTraceBundleUploader = class {
7312
7622
  }
7313
7623
  };
7314
7624
  function sha256Hex(body) {
7315
- return createHash3("sha256").update(body).digest("hex");
7625
+ return createHash5("sha256").update(body).digest("hex");
7316
7626
  }
7317
7627
  function readPositiveIntegerEnv2(name, fallback) {
7318
7628
  const value = process.env[name];
@@ -7491,6 +7801,7 @@ var DaemonCore = class {
7491
7801
  daemonVersion;
7492
7802
  chatBridgePath;
7493
7803
  slockCliPath;
7804
+ slockHome;
7494
7805
  runtimeDetector;
7495
7806
  agentManager;
7496
7807
  connection;
@@ -7505,6 +7816,8 @@ var DaemonCore = class {
7505
7816
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
7506
7817
  this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
7507
7818
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
7819
+ this.slockHome = resolveSlockHome();
7820
+ process.env[SLOCK_HOME_ENV] = this.slockHome;
7508
7821
  this.injectedTracer = Boolean(options.tracer);
7509
7822
  this.tracer = options.tracer ?? noopTracer;
7510
7823
  this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
@@ -7514,7 +7827,7 @@ var DaemonCore = class {
7514
7827
  });
7515
7828
  let connection;
7516
7829
  const agentManagerOptions = {
7517
- dataDir: options.dataDir,
7830
+ dataDir: options.dataDir ?? resolveSlockHomePath("agents", this.slockHome),
7518
7831
  serverUrl: options.serverUrl,
7519
7832
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
7520
7833
  slockCliPath: this.slockCliPath,
@@ -7536,7 +7849,7 @@ var DaemonCore = class {
7536
7849
  resolveMachineStateRoot() {
7537
7850
  if (this.options.machineStateDir) return this.options.machineStateDir;
7538
7851
  if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
7539
- return DEFAULT_MACHINE_STATE_ROOT;
7852
+ return resolveDefaultMachineStateRoot();
7540
7853
  }
7541
7854
  shouldEnableLocalTrace() {
7542
7855
  if (this.injectedTracer) return false;
@@ -7580,6 +7893,12 @@ var DaemonCore = class {
7580
7893
  }
7581
7894
  start() {
7582
7895
  logger.info("[Slock Daemon] Starting...");
7896
+ logger.info(`[Slock Daemon] ${SLOCK_HOME_ENV}=${this.slockHome}`);
7897
+ for (const legacy of listLegacySlockStatePaths(this.slockHome)) {
7898
+ logger.warn(
7899
+ `[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.`
7900
+ );
7901
+ }
7583
7902
  if (!this.machineLock) {
7584
7903
  this.machineLock = acquireDaemonMachineLock({
7585
7904
  apiKey: this.options.apiKey,