@slock-ai/daemon 0.46.0 → 0.46.1-staging.20260509070837

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.
@@ -34,7 +34,7 @@ var searchMessagesDeprecatedSchema = {
34
34
  };
35
35
  var listTasksDeprecatedSchema = {
36
36
  channel: z.string().describe("Deprecated channel argument."),
37
- status: z.enum(["all", "todo", "in_progress", "in_review", "done"]).optional().describe("Deprecated status argument.")
37
+ status: z.enum(["all", "todo", "in_progress", "in_review", "done", "closed"]).optional().describe("Deprecated status argument.")
38
38
  };
39
39
  var claimTasksDeprecatedSchema = {
40
40
  channel: z.string().describe("Deprecated channel argument."),
@@ -48,7 +48,7 @@ var unclaimTaskDeprecatedSchema = {
48
48
  var updateTaskStatusDeprecatedSchema = {
49
49
  channel: z.string().describe("Deprecated channel argument."),
50
50
  task_number: z.number().describe("Deprecated task number."),
51
- status: z.enum(["todo", "in_progress", "in_review", "done"]).describe("Deprecated status argument.")
51
+ status: z.enum(["todo", "in_progress", "in_review", "done", "closed"]).describe("Deprecated status argument.")
52
52
  };
53
53
  var DEPRECATED_MCP_TOOL_DEFINITIONS = [
54
54
  {
@@ -1087,7 +1087,7 @@ ${formatted}${footer}`
1087
1087
  "List all tasks in a channel. Returns each task's number, title, status, assignee, and message ID. Use this to see what work exists before claiming. Tasks marked as legacy are from an older system and cannot be claimed or modified.",
1088
1088
  {
1089
1089
  channel: z2.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
1090
- status: z2.enum(["all", "todo", "in_progress", "in_review", "done"]).default("all").describe("Filter by status (default: all)")
1090
+ status: z2.enum(["all", "todo", "in_progress", "in_review", "done", "closed"]).default("all").describe("Filter by status (default: all)")
1091
1091
  },
1092
1092
  async ({ channel, status }) => {
1093
1093
  try {
@@ -1311,11 +1311,11 @@ ${lines.join("\n")}${threadHint}`
1311
1311
  );
1312
1312
  server.tool(
1313
1313
  "update_task_status",
1314
- "Update a task's progress status. You must be the task's assignee to update it. Use in_review when your work is ready for human validation. Only set done for trivial tasks or after explicit approval. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review or done, in_review\u2192done or back to in_progress.",
1314
+ "Update a task's progress status. You must be the task's assignee to update it. Use in_review when your work is ready for human validation. Only set done for trivial tasks or after explicit approval. Use closed to mark a task as won't-do (cancelled / abandoned / out-of-scope) \u2014 distinct from done. Valid transitions: todo\u2192{in_progress,closed}, in_progress\u2192{in_review,done,closed}, in_review\u2192{done,in_progress,closed}, done\u2192{todo,in_progress,in_review,closed}, closed\u2192todo (reopen).",
1315
1315
  {
1316
1316
  channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1317
1317
  task_number: z2.number().describe("The task number to update (e.g. 3)"),
1318
- status: z2.enum(["todo", "in_progress", "in_review", "done"]).describe("The new status")
1318
+ status: z2.enum(["todo", "in_progress", "in_review", "done", "closed"]).describe("The new status")
1319
1319
  },
1320
1320
  async ({ channel, task_number, status }) => {
1321
1321
  try {
@@ -2558,8 +2558,8 @@ function runCursorModelsCommand() {
2558
2558
  }
2559
2559
 
2560
2560
  // src/drivers/gemini.ts
2561
- import { spawn as spawn5 } from "child_process";
2562
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2561
+ import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
2562
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
2563
2563
  import path7 from "path";
2564
2564
  function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2565
2565
  const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
@@ -2568,6 +2568,71 @@ function buildGeminiSpawnEnv(ctx, platform = process.platform) {
2568
2568
  }
2569
2569
  return spawnEnv;
2570
2570
  }
2571
+ function normalizeExecOutput2(raw) {
2572
+ return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
2573
+ }
2574
+ function buildGeminiArgs(config) {
2575
+ const args = [
2576
+ "--output-format",
2577
+ "stream-json",
2578
+ "--yolo",
2579
+ // Gemini CLI headless mode is selected by -p/--prompt. Keep the actual
2580
+ // prompt off argv and feed it through stdin below; this avoids Windows
2581
+ // cmd.exe's 8191-character command-line limit and keeps long wake payloads
2582
+ // below CreateProcess argv pressure too.
2583
+ "-p",
2584
+ ""
2585
+ ];
2586
+ if (config.model && config.model !== "default") {
2587
+ args.push("--model", config.model);
2588
+ }
2589
+ if (config.sessionId) {
2590
+ args.push("--resume", config.sessionId);
2591
+ }
2592
+ return args;
2593
+ }
2594
+ function resolveGeminiSpawn(commandArgs, deps = {}) {
2595
+ const platform = deps.platform ?? process.platform;
2596
+ if (platform !== "win32") {
2597
+ return { command: resolveCommandOnPath("gemini", deps) ?? "gemini", args: commandArgs };
2598
+ }
2599
+ const execFileSyncFn = deps.execFileSyncFn ?? execFileSync2;
2600
+ const existsSyncFn = deps.existsSyncFn ?? existsSync5;
2601
+ const env = deps.env ?? process.env;
2602
+ const winPath = path7.win32;
2603
+ let geminiEntry = null;
2604
+ try {
2605
+ const globalRoot = normalizeExecOutput2(execFileSyncFn("npm", ["root", "-g"], {
2606
+ encoding: "utf8",
2607
+ stdio: ["ignore", "pipe", "pipe"],
2608
+ env
2609
+ })).trim();
2610
+ const candidate = winPath.join(globalRoot, "@google", "gemini-cli", "bundle", "gemini.js");
2611
+ if (existsSyncFn(candidate)) geminiEntry = candidate;
2612
+ } catch {
2613
+ }
2614
+ if (!geminiEntry) {
2615
+ try {
2616
+ const cmdPath = normalizeExecOutput2(execFileSyncFn("where.exe", ["gemini"], {
2617
+ encoding: "utf8",
2618
+ stdio: ["ignore", "pipe", "pipe"],
2619
+ env
2620
+ })).trim().split(/\r?\n/)[0];
2621
+ const candidate = winPath.join(winPath.dirname(cmdPath), "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
2622
+ if (existsSyncFn(candidate)) geminiEntry = candidate;
2623
+ } catch {
2624
+ }
2625
+ }
2626
+ if (!geminiEntry) {
2627
+ throw new Error(
2628
+ "Cannot resolve Gemini CLI entry point on Windows. Ensure @google/gemini-cli is installed globally via npm (npm i -g @google/gemini-cli)."
2629
+ );
2630
+ }
2631
+ return {
2632
+ command: process.execPath,
2633
+ args: [geminiEntry, ...commandArgs]
2634
+ };
2635
+ }
2571
2636
  var GeminiDriver = class {
2572
2637
  id = "gemini";
2573
2638
  lifecycle = {
@@ -2597,26 +2662,15 @@ var GeminiDriver = class {
2597
2662
  this.sessionId = ctx.config.sessionId || null;
2598
2663
  this.sessionAnnounced = false;
2599
2664
  this.writeGeminiSettings(ctx);
2600
- const args = [
2601
- "--output-format",
2602
- "stream-json",
2603
- "--yolo",
2604
- "-p",
2605
- ctx.prompt
2606
- ];
2607
- if (ctx.config.model && ctx.config.model !== "default") {
2608
- args.push("--model", ctx.config.model);
2609
- }
2610
- if (ctx.config.sessionId) {
2611
- args.push("--resume", ctx.config.sessionId);
2612
- }
2665
+ const { command, args } = resolveGeminiSpawn(buildGeminiArgs(ctx.config));
2613
2666
  const spawnEnv = buildGeminiSpawnEnv(ctx);
2614
- const proc = spawn5("gemini", args, {
2667
+ const proc = spawn5(command, args, {
2615
2668
  cwd: ctx.workingDirectory,
2616
2669
  stdio: ["pipe", "pipe", "pipe"],
2617
2670
  env: spawnEnv,
2618
- shell: process.platform === "win32"
2671
+ shell: false
2619
2672
  });
2673
+ proc.stdin?.end(ctx.prompt);
2620
2674
  return { process: proc };
2621
2675
  }
2622
2676
  parseLine(line) {
@@ -2718,7 +2772,7 @@ var GeminiDriver = class {
2718
2772
  // src/drivers/kimi.ts
2719
2773
  import { randomUUID } from "crypto";
2720
2774
  import { spawn as spawn6 } from "child_process";
2721
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2775
+ import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
2722
2776
  import os3 from "os";
2723
2777
  import path8 from "path";
2724
2778
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
@@ -2783,7 +2837,7 @@ var KimiDriver = class {
2783
2837
  const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
2784
2838
  const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
2785
2839
  const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
2786
- if (!isResume || !existsSync5(systemPromptPath)) {
2840
+ if (!isResume || !existsSync6(systemPromptPath)) {
2787
2841
  writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
2788
2842
  }
2789
2843
  writeFileSync6(agentFilePath, [
@@ -2972,6 +3026,14 @@ var SLOCK_AGENT_NAME = "slock";
2972
3026
  var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2973
3027
  var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2974
3028
  var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
3029
+ var OPENCODE_PROVIDER_LABELS = {
3030
+ opencode: "OpenCode",
3031
+ "opencode-go": "OpenCode Go",
3032
+ openai: "OpenAI",
3033
+ openrouter: "OpenRouter",
3034
+ deepseek: "DeepSeek",
3035
+ fusecode: "FuseCode"
3036
+ };
2975
3037
  function buildChatBridgeCommand(ctx) {
2976
3038
  const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2977
3039
  return [
@@ -3118,12 +3180,62 @@ function parseOpenCodeModelsOutput(output) {
3118
3180
  seen.add(line);
3119
3181
  models.push({
3120
3182
  id: line,
3121
- label: line,
3183
+ label: formatOpenCodeModelLabel(line),
3122
3184
  verified: "launchable"
3123
3185
  });
3124
3186
  }
3125
3187
  return models.length > 0 ? { models } : null;
3126
3188
  }
3189
+ function formatOpenCodeModelLabel(modelId) {
3190
+ const separatorIndex = modelId.indexOf("/");
3191
+ if (separatorIndex <= 0 || separatorIndex === modelId.length - 1) return modelId;
3192
+ const providerId = modelId.slice(0, separatorIndex);
3193
+ const modelName = modelId.slice(separatorIndex + 1);
3194
+ const providerLabel = OPENCODE_PROVIDER_LABELS[providerId] || humanizeOpenCodeSegment(providerId);
3195
+ const modelParts = modelName.split("/");
3196
+ const modelLabel = humanizeOpenCodeSegment(modelParts[modelParts.length - 1] || modelName);
3197
+ if (modelParts.length === 1) return `${modelLabel} \xB7 ${providerLabel}`;
3198
+ const upstreamLabel = modelParts.slice(0, -1).map(humanizeOpenCodeSegment).join(" / ");
3199
+ return `${modelLabel} \xB7 ${upstreamLabel} via ${providerLabel}`;
3200
+ }
3201
+ function humanizeOpenCodeSegment(value) {
3202
+ return value.replace(/\[(\d+)m\]/gi, "-$1m").split(/[-_]/).filter(Boolean).map(formatOpenCodeLabelToken).join(" ");
3203
+ }
3204
+ function formatOpenCodeLabelToken(token) {
3205
+ const normalized = token.toLowerCase();
3206
+ const specialCases = {
3207
+ ai: "AI",
3208
+ api: "API",
3209
+ b: "B",
3210
+ chatgpt: "ChatGPT",
3211
+ claude: "Claude",
3212
+ codestral: "Codestral",
3213
+ deepseek: "DeepSeek",
3214
+ flash: "Flash",
3215
+ free: "Free",
3216
+ gemini: "Gemini",
3217
+ glm: "GLM",
3218
+ gpt: "GPT",
3219
+ hy3: "HY3",
3220
+ kimi: "Kimi",
3221
+ m: "M",
3222
+ minimax: "MiniMax",
3223
+ nano: "Nano",
3224
+ nemotron: "Nemotron",
3225
+ omni: "Omni",
3226
+ opus: "Opus",
3227
+ pro: "Pro",
3228
+ sonnet: "Sonnet",
3229
+ super: "Super"
3230
+ };
3231
+ if (specialCases[normalized]) return specialCases[normalized];
3232
+ if (/^v\d+(\.\d+)?$/.test(normalized)) return normalized.toUpperCase();
3233
+ if (/^\d+m$/i.test(token)) return token.toUpperCase();
3234
+ if (/^\d+[bk]$/i.test(token)) return token.toUpperCase();
3235
+ if (/^m\d+(\.\d+)?$/i.test(token)) return token.toUpperCase();
3236
+ if (/^\d/.test(token)) return token;
3237
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
3238
+ }
3127
3239
  function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
3128
3240
  const commandResult = runCommand(home);
3129
3241
  if (commandResult.error || commandResult.status !== 0) return null;
@@ -4183,6 +4295,66 @@ function isMissingResumeSession(ap) {
4183
4295
  }
4184
4296
  return false;
4185
4297
  }
4298
+ function classifyActivityDetailForTrace(detail) {
4299
+ if (!detail) return void 0;
4300
+ if (detail === "Message received") return "message_received";
4301
+ if (detail === "Starting\u2026") return "starting";
4302
+ if (detail === "Running command\u2026") return "running_command";
4303
+ if (detail === "Checking messages\u2026") return "checking_messages";
4304
+ if (detail === "Compacting context") return "compacting_context";
4305
+ if (detail === "Context compaction finished") return "compaction_finished";
4306
+ if (detail === "Context compaction still running; no finish event observed") return "compaction_stale";
4307
+ if (detail === "Idle" || detail === "Process idle") return "idle";
4308
+ if (detail.startsWith("Restarting stalled ") && detail.endsWith(" runtime for queued message")) return "stalled_recovery";
4309
+ if (detail.startsWith("Runtime stalled: no runtime events for ")) return "runtime_stalled";
4310
+ return "other";
4311
+ }
4312
+ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
4313
+ const context = [];
4314
+ if (ap.lastActivityDetail) {
4315
+ context.push(`after ${ap.lastActivityDetail}`);
4316
+ }
4317
+ if (ap.driver.busyDeliveryMode === "gated") {
4318
+ context.push(`phase=${ap.gatedSteering.phase}`);
4319
+ }
4320
+ if (ap.gatedSteering.outstandingToolUses > 0) {
4321
+ context.push(`tools=${ap.gatedSteering.outstandingToolUses}`);
4322
+ }
4323
+ if (ap.gatedSteering.compacting) {
4324
+ context.push("compacting");
4325
+ }
4326
+ if (ap.inbox.length > 0) {
4327
+ context.push(`queued=${ap.inbox.length}`);
4328
+ }
4329
+ const detail = [
4330
+ `Runtime stalled: no runtime events for ${staleForMinutes}m`,
4331
+ context.length > 0 ? ` (${context.join(", ")})` : ""
4332
+ ].join("");
4333
+ return {
4334
+ detail,
4335
+ traceAttrs: {
4336
+ ageMs: staleForMs,
4337
+ staleForMinutes,
4338
+ lastActivity: ap.lastActivity,
4339
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
4340
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
4341
+ runtime: ap.config.runtime,
4342
+ model: ap.config.model,
4343
+ launchId: ap.launchId || void 0,
4344
+ sessionIdPresent: Boolean(ap.sessionId),
4345
+ inboxCount: ap.inbox.length,
4346
+ pendingNotificationCount: ap.pendingNotificationCount,
4347
+ processPidPresent: typeof ap.process.pid === "number",
4348
+ busyDeliveryMode: ap.driver.busyDeliveryMode,
4349
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
4350
+ gatedPhase: ap.driver.busyDeliveryMode === "gated" ? ap.gatedSteering.phase : void 0,
4351
+ outstandingToolUses: ap.gatedSteering.outstandingToolUses,
4352
+ compacting: ap.gatedSteering.compacting,
4353
+ recentStderrCount: ap.recentStderr.length,
4354
+ recentStdoutCount: ap.recentStdout.length
4355
+ }
4356
+ };
4357
+ }
4186
4358
  function getMessageDeliveryText(driver) {
4187
4359
  return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
4188
4360
  }
@@ -5855,17 +6027,16 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5855
6027
  if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
5856
6028
  ap.runtimeProgressStaleSince = Date.now();
5857
6029
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
5858
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
5859
- ageMs: staleForMs,
5860
- staleForMinutes,
5861
- lastActivity: ap.lastActivity
5862
- });
6030
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
6031
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", diagnostic.traceAttrs);
5863
6032
  this.endRuntimeTrace(ap, "error", {
5864
6033
  outcome: "runtime-stalled",
5865
6034
  ageMs: staleForMs,
5866
- lastActivity: ap.lastActivity
6035
+ lastActivity: ap.lastActivity,
6036
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6037
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
5867
6038
  });
5868
- this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
6039
+ this.broadcastActivity(agentId, "error", diagnostic.detail);
5869
6040
  return true;
5870
6041
  }
5871
6042
  recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
@@ -5881,10 +6052,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5881
6052
  if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
5882
6053
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
5883
6054
  ap.runtimeProgressStaleSince ??= Date.now();
6055
+ const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
5884
6056
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
5885
- ageMs: staleForMs,
5886
- staleForMinutes,
5887
- lastActivity: ap.lastActivity,
6057
+ ...diagnostic.traceAttrs,
5888
6058
  pendingMessages: ap.inbox.length,
5889
6059
  recovery: "terminate_for_queued_message"
5890
6060
  });
@@ -5892,6 +6062,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5892
6062
  outcome: "runtime-stalled",
5893
6063
  ageMs: staleForMs,
5894
6064
  lastActivity: ap.lastActivity,
6065
+ lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
6066
+ lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
5895
6067
  pendingMessages: ap.inbox.length,
5896
6068
  recovery: "terminate_for_queued_message"
5897
6069
  });
@@ -6658,6 +6830,7 @@ function acquireDaemonMachineLock(options) {
6658
6830
  import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
6659
6831
  import path13 from "path";
6660
6832
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
6833
+ var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
6661
6834
  var DEFAULT_MAX_FILES = 8;
6662
6835
  var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6663
6836
  "serverId",
@@ -6674,14 +6847,25 @@ var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
6674
6847
  var LocalRotatingTraceSink = class {
6675
6848
  traceDir;
6676
6849
  maxFileBytes;
6850
+ maxFileAgeMs;
6677
6851
  maxFiles;
6852
+ nowMsProvider;
6678
6853
  currentFile = null;
6854
+ currentFileOpenedAtMs = null;
6679
6855
  currentSize = 0;
6680
6856
  sequence = 0;
6681
6857
  constructor(options) {
6682
6858
  this.traceDir = path13.join(options.machineDir, "traces");
6683
6859
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
6860
+ const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
6861
+ const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
6862
+ this.maxFileAgeMs = baseAgeMs + ageJitterMs;
6684
6863
  this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
6864
+ this.nowMsProvider = options.nowMsProvider ?? Date.now;
6865
+ }
6866
+ /** Exposed for observability — the effective rotation age after jitter. */
6867
+ getMaxFileAgeMs() {
6868
+ return this.maxFileAgeMs;
6685
6869
  }
6686
6870
  record(span) {
6687
6871
  try {
@@ -6698,13 +6882,16 @@ var LocalRotatingTraceSink = class {
6698
6882
  }
6699
6883
  ensureFile(nextBytes) {
6700
6884
  mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
6701
- if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
6885
+ const nowMs = this.nowMsProvider();
6886
+ const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
6887
+ if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
6702
6888
  this.currentFile = path13.join(
6703
6889
  this.traceDir,
6704
- `daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6890
+ `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
6705
6891
  );
6706
6892
  writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
6707
6893
  this.currentSize = statSync4(this.currentFile).size;
6894
+ this.currentFileOpenedAtMs = nowMs;
6708
6895
  this.pruneOldFiles();
6709
6896
  }
6710
6897
  }
@@ -6789,7 +6976,7 @@ function isDiagnosticIdAttr(key) {
6789
6976
  }
6790
6977
 
6791
6978
  // src/traceBundleUpload.ts
6792
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
6979
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
6793
6980
  import { gzipSync } from "zlib";
6794
6981
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
6795
6982
  import path14 from "path";
@@ -6918,6 +7105,35 @@ async function uploadWithSignedCapability({
6918
7105
  return { capability, session, uploadResponse };
6919
7106
  }
6920
7107
 
7108
+ // src/traceJitter.ts
7109
+ import { createHash as createHash2 } from "crypto";
7110
+ var INITIAL_UPLOAD_DELAY_SPAN_MS = 3e4;
7111
+ var UPLOAD_INTERVAL_JITTER_SPAN_MS = 6e4;
7112
+ var MAX_FILE_AGE_JITTER_SPAN_MS = 6e4;
7113
+ function computeTraceJitter(lockId) {
7114
+ const seed = createHash2("sha256").update(lockId).digest();
7115
+ return {
7116
+ initialUploadDelayMs: seed.readUInt32BE(0) % INITIAL_UPLOAD_DELAY_SPAN_MS,
7117
+ uploadIntervalJitterMs: seed.readUInt32BE(4) % UPLOAD_INTERVAL_JITTER_SPAN_MS,
7118
+ maxFileAgeJitterMs: seed.readUInt32BE(8) % MAX_FILE_AGE_JITTER_SPAN_MS
7119
+ };
7120
+ }
7121
+ var NO_JITTER = {
7122
+ initialUploadDelayMs: 0,
7123
+ uploadIntervalJitterMs: 0,
7124
+ maxFileAgeJitterMs: 0
7125
+ };
7126
+ function bucketDelayMs(delayMs) {
7127
+ if (delayMs < 1e3) return "0-1s";
7128
+ if (delayMs < 5e3) return "1-5s";
7129
+ if (delayMs < 15e3) return "5-15s";
7130
+ if (delayMs < 3e4) return "15-30s";
7131
+ if (delayMs < 6e4) return "30-60s";
7132
+ if (delayMs < 3e5) return "60s-5m";
7133
+ if (delayMs < 6e5) return "5-10m";
7134
+ return "10m+";
7135
+ }
7136
+
6921
7137
  // src/traceBundleUpload.ts
6922
7138
  var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
6923
7139
  var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
@@ -6925,30 +7141,66 @@ var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
6925
7141
  var DEFAULT_MAX_FILES_PER_RUN = 4;
6926
7142
  var DaemonTraceBundleUploader = class {
6927
7143
  options;
6928
- timer = null;
7144
+ jitter;
7145
+ timers;
7146
+ initialDelayTimer = null;
7147
+ intervalTimer = null;
7148
+ stopped = false;
6929
7149
  constructor(options) {
6930
7150
  this.options = options;
7151
+ this.jitter = options.jitter ?? (options.lockId ? computeTraceJitter(options.lockId) : NO_JITTER);
7152
+ this.timers = options.timers ?? {
7153
+ setTimeout: globalThis.setTimeout.bind(globalThis),
7154
+ setInterval: globalThis.setInterval.bind(globalThis),
7155
+ clearTimeout: globalThis.clearTimeout.bind(globalThis),
7156
+ clearInterval: globalThis.clearInterval.bind(globalThis)
7157
+ };
6931
7158
  }
6932
7159
  start() {
6933
- if (this.timer) return;
6934
- void this.uploadOnce();
6935
- this.timer = setInterval(() => {
6936
- void this.uploadOnce();
6937
- }, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
7160
+ if (this.stopped) return;
7161
+ if (this.initialDelayTimer || this.intervalTimer) return;
7162
+ const initialDelayMs = this.jitter.initialUploadDelayMs;
7163
+ this.initialDelayTimer = this.timers.setTimeout(() => {
7164
+ this.initialDelayTimer = null;
7165
+ if (this.stopped) return;
7166
+ void this.uploadOnce("initial");
7167
+ this.scheduleNextTick();
7168
+ }, initialDelayMs);
6938
7169
  }
6939
7170
  stop() {
6940
- if (!this.timer) return;
6941
- clearInterval(this.timer);
6942
- this.timer = null;
7171
+ this.stopped = true;
7172
+ if (this.initialDelayTimer) {
7173
+ this.timers.clearTimeout(this.initialDelayTimer);
7174
+ this.initialDelayTimer = null;
7175
+ }
7176
+ if (this.intervalTimer) {
7177
+ this.timers.clearTimeout(this.intervalTimer);
7178
+ this.intervalTimer = null;
7179
+ }
6943
7180
  }
6944
- async uploadOnce() {
7181
+ /**
7182
+ * Drive a single upload pass. `trigger` is surfaced as a span attribute so
7183
+ * we can distinguish startup drain vs steady-state ticks in ScopeDB.
7184
+ */
7185
+ async uploadOnce(trigger = "manual") {
6945
7186
  const files = await this.findUploadCandidates();
6946
7187
  let uploaded = 0;
6947
7188
  for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
6948
- if (await this.uploadFile(file)) uploaded += 1;
7189
+ if (await this.uploadFile(file, trigger)) uploaded += 1;
6949
7190
  }
6950
7191
  return { attempted: files.length, uploaded };
6951
7192
  }
7193
+ scheduleNextTick() {
7194
+ if (this.stopped) return;
7195
+ const baseIntervalMs = this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS);
7196
+ const nextMs = baseIntervalMs + this.jitter.uploadIntervalJitterMs;
7197
+ this.intervalTimer = this.timers.setTimeout(() => {
7198
+ this.intervalTimer = null;
7199
+ if (this.stopped) return;
7200
+ void this.uploadOnce("interval");
7201
+ this.scheduleNextTick();
7202
+ }, nextMs);
7203
+ }
6952
7204
  async findUploadCandidates() {
6953
7205
  const traceDir = path14.join(this.options.machineDir, "traces");
6954
7206
  let names;
@@ -6975,13 +7227,16 @@ var DaemonTraceBundleUploader = class {
6975
7227
  }
6976
7228
  return candidates;
6977
7229
  }
6978
- async uploadFile(file) {
7230
+ async uploadFile(file, trigger) {
6979
7231
  const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
6980
7232
  surface: "daemon",
6981
7233
  kind: "producer",
6982
7234
  attrs: {
6983
7235
  file_present: true,
6984
- worker_url_present: Boolean(this.options.workerUrl)
7236
+ worker_url_present: Boolean(this.options.workerUrl),
7237
+ upload_trigger: trigger,
7238
+ initial_delay_ms_bucket: bucketDelayMs(this.jitter.initialUploadDelayMs),
7239
+ interval_jitter_ms_bucket: bucketDelayMs(this.jitter.uploadIntervalJitterMs)
6985
7240
  }
6986
7241
  });
6987
7242
  try {
@@ -7057,7 +7312,7 @@ var DaemonTraceBundleUploader = class {
7057
7312
  }
7058
7313
  };
7059
7314
  function sha256Hex(body) {
7060
- return createHash2("sha256").update(body).digest("hex");
7315
+ return createHash3("sha256").update(body).digest("hex");
7061
7316
  }
7062
7317
  function readPositiveIntegerEnv2(name, fallback) {
7063
7318
  const value = process.env[name];
@@ -7067,6 +7322,7 @@ function readPositiveIntegerEnv2(name, fallback) {
7067
7322
  }
7068
7323
 
7069
7324
  // src/core.ts
7325
+ var DEFAULT_TRACE_UPLOAD_URL = "https://slock-trace-upload.botiverse.dev";
7070
7326
  var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
7071
7327
  function parseDaemonCliArgs(args) {
7072
7328
  let serverUrl = "";
@@ -7287,11 +7543,18 @@ var DaemonCore = class {
7287
7543
  if (!this.options.localTrace) return false;
7288
7544
  return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
7289
7545
  }
7546
+ resolveTraceJitter() {
7547
+ const lockId = this.machineLock?.lockId;
7548
+ return lockId ? computeTraceJitter(lockId) : NO_JITTER;
7549
+ }
7290
7550
  installLocalTraceSink(machineDir) {
7291
7551
  if (!this.shouldEnableLocalTrace()) return;
7552
+ const jitter = this.resolveTraceJitter();
7292
7553
  this.localTraceSink = new LocalRotatingTraceSink({
7293
7554
  machineDir,
7294
7555
  maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
7556
+ maxFileAgeMs: this.options.localTraceMaxFileAgeMs ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_AGE_MS", 5 * 60 * 1e3),
7557
+ maxFileAgeJitterMs: jitter.maxFileAgeJitterMs,
7295
7558
  maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
7296
7559
  });
7297
7560
  this.tracer = new BasicTracer({
@@ -7302,15 +7565,16 @@ var DaemonCore = class {
7302
7565
  installTraceBundleUploader(machineDir) {
7303
7566
  if (!this.shouldEnableLocalTrace()) return;
7304
7567
  if (this.traceBundleUploader) return;
7305
- const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
7306
- if (!workerUrl) return;
7568
+ if (process.env.SLOCK_DAEMON_TRACE_UPLOAD_DISABLED === "1") return;
7569
+ const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL || DEFAULT_TRACE_UPLOAD_URL;
7307
7570
  this.traceBundleUploader = new DaemonTraceBundleUploader({
7308
7571
  machineDir,
7309
7572
  serverUrl: this.options.serverUrl,
7310
7573
  apiKey: this.options.apiKey,
7311
7574
  workerUrl,
7312
7575
  tracer: this.tracer,
7313
- currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
7576
+ currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null,
7577
+ lockId: this.machineLock?.lockId
7314
7578
  });
7315
7579
  this.traceBundleUploader.start();
7316
7580
  }
package/dist/cli/index.js CHANGED
@@ -1119,7 +1119,7 @@ function formatTaskStatusUpdated(taskNumber, status) {
1119
1119
  }
1120
1120
 
1121
1121
  // src/commands/task/list.ts
1122
- var VALID_STATUSES = /* @__PURE__ */ new Set(["all", "todo", "in_progress", "in_review", "done"]);
1122
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["all", "todo", "in_progress", "in_review", "done", "closed"]);
1123
1123
  function registerTaskListCommand(parent) {
1124
1124
  parent.command("list").description("List tasks in a channel").requiredOption("--channel <target>", "Channel target: '#channel'").option("--status <s>", "Filter: all|todo|in_progress|in_review|done (default: server-side)").action(async (opts) => {
1125
1125
  let ctx;
@@ -1253,7 +1253,7 @@ function registerTaskUnclaimCommand(parent) {
1253
1253
  }
1254
1254
 
1255
1255
  // src/commands/task/update.ts
1256
- var STATUSES = ["todo", "in_progress", "in_review", "done"];
1256
+ var STATUSES = ["todo", "in_progress", "in_review", "done", "closed"];
1257
1257
  function registerTaskUpdateCommand(parent) {
1258
1258
  parent.command("update").description("Update task status").requiredOption("--channel <target>", "Channel target: '#channel'").requiredOption("--number <n>", "Task number to update").requiredOption(
1259
1259
  "--status <status>",
package/dist/core.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSlockCliPath,
10
10
  resolveWorkspaceDirectoryPath,
11
11
  scanWorkspaceDirectories
12
- } from "./chunk-Q4XUZB34.js";
12
+ } from "./chunk-FG5JGA67.js";
13
13
  import {
14
14
  subscribeDaemonLogs
15
15
  } from "./chunk-Z3PCMYZO.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DAEMON_CLI_USAGE,
4
4
  DaemonCore,
5
5
  parseDaemonCliArgs
6
- } from "./chunk-Q4XUZB34.js";
6
+ } from "./chunk-FG5JGA67.js";
7
7
  import "./chunk-Z3PCMYZO.js";
8
8
 
9
9
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.46.0",
3
+ "version": "0.46.1-staging.20260509070837",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"