open-walnut 0.6.5 → 0.6.6-nightly.20261007.78

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.
Files changed (55) hide show
  1. package/dist/actions/inbox-triage-batch.js +65 -33
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli-fast.js +575 -152
  4. package/dist/cli.js +68991 -59866
  5. package/dist/daemon-binaries/acp-worker.js.version +1 -1
  6. package/dist/daemon-binaries/dtach-linux-x64 +0 -0
  7. package/dist/daemon-binaries/external-scan-core.cjs +6 -5
  8. package/dist/daemon-binaries/trigger-check-core.cjs +28 -4
  9. package/dist/data/skills/walnut/SKILL.md +32 -19
  10. package/dist/data/skills/walnut-board/SKILL.md +6 -2
  11. package/dist/data/skills/walnut-health-sleep-report/SKILL.md +3 -1
  12. package/dist/data/skills/walnut-import-external-sessions/SKILL.md +6 -4
  13. package/dist/data/skills/walnut-inbox-triage/SKILL.md +33 -39
  14. package/dist/data/skills/walnut-places/SKILL.md +58 -0
  15. package/dist/data/skills/walnut-self-knowledge/SKILL.md +1 -1
  16. package/dist/data/skills/walnut-session-messaging/SKILL.md +21 -16
  17. package/dist/data/skills/walnut-trigger/SKILL.md +99 -37
  18. package/dist/hooks/on-compact.js +18 -4
  19. package/dist/hooks/on-stop.js +18 -4
  20. package/dist/integrations/calendar/index.js +19 -7
  21. package/dist/integrations/jira/index.js +90 -8
  22. package/dist/integrations/mail/agent-skills/walnut-mail/SKILL.md +15 -9
  23. package/dist/integrations/mail/index.js +84 -2
  24. package/dist/integrations/ms-todo/index.js +90 -8
  25. package/dist/integrations/walnut-time/dist/web.mjs +211 -7
  26. package/dist/integrations/walnut-time/manifest.json +1 -1
  27. package/dist/session-server/index.js +20 -5
  28. package/dist/web/server.js +29703 -20634
  29. package/dist/web/static/assets/{OfficePreview-CW4YDiaD.js → OfficePreview-Cus2vVQN.js} +2 -2
  30. package/dist/web/static/assets/{docx-preview-BlM6tuxh.js → docx-preview-CcO1IhKq.js} +1 -1
  31. package/dist/web/static/assets/{index-CZh_2gpd.js → index-9FopmG-D.js} +1 -1
  32. package/dist/web/static/assets/{index-DOmDc0sH.js → index-AYUksouX.js} +1 -1
  33. package/dist/web/static/assets/{index-6e7UqBsu.js → index-B0uD_WeU.js} +1 -1
  34. package/dist/web/static/assets/index-B22BJLsJ.css +32 -0
  35. package/dist/web/static/assets/{index-DIA_0LSF.js → index-B9zziRy8.js} +1 -1
  36. package/dist/web/static/assets/{index-CCh_7KF9.js → index-BgEdzzjn.js} +1 -1
  37. package/dist/web/static/assets/{index-BcD90s9A.js → index-BqDjc8vb.js} +1 -1
  38. package/dist/web/static/assets/{index-4QfSx_Hf.js → index-BvQeq5RX.js} +1 -1
  39. package/dist/web/static/assets/{index-D8T5pRWU.js → index-BveMl4Vh.js} +1 -1
  40. package/dist/web/static/assets/{index-D2RIGe6z.js → index-CSzE4_om.js} +1 -1
  41. package/dist/web/static/assets/{index-QIv6rjPi.js → index-Ceow8jai.js} +1 -1
  42. package/dist/web/static/assets/{index-DPi6pqIF.js → index-ClR5rzCe.js} +1 -1
  43. package/dist/web/static/assets/{index-9sdVyhsn.js → index-DIZrZTpH.js} +1 -1
  44. package/dist/web/static/assets/{index-BksaB5GC.js → index-DXZ1Z93P.js} +1 -1
  45. package/dist/web/static/assets/{index-CI8hsb9E.js → index-DfdRNC1A.js} +1 -1
  46. package/dist/web/static/assets/{index-A5WamBLC.js → index-DtNzEUu6.js} +438 -427
  47. package/dist/web/static/assets/{index-DjCa9ycs.js → index-Dz2JY7E9.js} +1 -1
  48. package/dist/web/static/assets/{index-xVStWzmf.js → index-Q2hsD9N_.js} +1 -1
  49. package/dist/web/static/assets/{index-D0jai_t_.js → index-YfsaZthT.js} +1 -1
  50. package/dist/web/static/assets/{jszip.min-CPWKtQ6u.js → jszip.min-0xzyHQZu.js} +1 -1
  51. package/dist/web/static/assets/{pptx-preview.es-D0UnOmO4.js → pptx-preview.es-CC9d0JgL.js} +1 -1
  52. package/dist/web/static/index.html +2 -2
  53. package/dist/workers/git-compaction-worker.js +21 -7
  54. package/package.json +1 -1
  55. package/dist/web/static/assets/index-COeIq339.css +0 -32
@@ -18,7 +18,7 @@ function resolveOpenWalnutHome() {
18
18
  const productionHome = path.join(os.homedir(), ".open-walnut");
19
19
  const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
20
20
  if (isTestEnv) {
21
- if (envHome && envHome !== productionHome && !envHome.startsWith(productionHome + path.sep)) {
21
+ if (envHome && !isUnderProductionHome(envHome)) {
22
22
  assertNotProductionPath(envHome);
23
23
  return envHome;
24
24
  }
@@ -43,8 +43,7 @@ function assertNotProductionPath(inputPath) {
43
43
  const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
44
44
  if (!isTestEnv) return;
45
45
  const resolved = path.resolve(inputPath);
46
- const prodHome = path.join(os.homedir(), ".open-walnut");
47
- if (resolved === prodHome || resolved.startsWith(prodHome + path.sep)) {
46
+ if (isUnderProductionHome(resolved)) {
48
47
  throw new Error(
49
48
  `SAFETY: Test process attempted to use production path: ${resolved}
50
49
  This would destroy real user data in ~/.open-walnut/.
@@ -52,6 +51,18 @@ function assertNotProductionPath(inputPath) {
52
51
  );
53
52
  }
54
53
  }
54
+ function isUnderProductionHome(p) {
55
+ const homes = [os.homedir()];
56
+ try {
57
+ homes.push(os.userInfo().homedir);
58
+ } catch {
59
+ }
60
+ const resolved = path.resolve(p);
61
+ return homes.some((h) => {
62
+ const prodHome = path.join(h, ".open-walnut");
63
+ return resolved === prodHome || resolved.startsWith(prodHome + path.sep);
64
+ });
65
+ }
55
66
  function isEphemeralTmpDir(inputPath) {
56
67
  if (!/[\\/]open-walnut-[^\\/]+$/.test(inputPath)) return false;
57
68
  const tmpDirs = /* @__PURE__ */ new Set();
@@ -80,7 +91,7 @@ function isWalnutSourceCheckout(dir) {
80
91
  return false;
81
92
  }
82
93
  }
83
- var WALNUT_HOME, IS_EPHEMERAL, CLOUD_MODE, TASKS_DIR, TASKS_FILE, ARCHIVE_DIR, MEMORY_DIR, SESSIONS_DIR, PROJECTS_DIR, CONFIG_FILE, SYNC_DIR, PLUGIN_STORES_DIR, SESSIONS_FILE, CLAUDE_HOME, HISTORY_CACHE_DIR, PROJECTION_CACHE_DIR, TRANSCRIPT_CACHE_DIR, TASK_QUEUE_DIR, CONTROL_QUEUE_DIR, SEND_QUEUE_DIR, HUMAN_INBOX_QUEUE_DIR, HOST_MODEL_CATALOG_FILE, HOOK_LOG_FILE, DAILY_DIR, MEMORY_FILE, USER_FILE, LEGACY_MEMORY_FILE, PROJECTS_MEMORY_DIR, CHAT_HISTORY_FILE, CONVERSATIONS_DIR, GLOBAL_SKILLS_DIR, SKILL_SETTINGS_FILE, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_PLUGINS_DIR, CLAUDE_CREDENTIALS_FILE, CRON_FILE, USAGE_DB_FILE, LOG_DIR, SESSION_STREAMS_DIR, DTACH_SOCKET_DIR, SESSION_QUEUE_FILE, IMAGES_DIR, REMOTE_IMAGES_DIR, MOBILE_STAGED_IMAGES_DIR, PASTES_DIR, HEARTBEAT_FILE, COMMANDS_DIR, BUILTIN_COMMANDS_DIR, BUILTIN_SKILLS_DIR, FREQUENT_DIRS_FILE, ASK_WALNUT_LAUNCH_FILE, MENTION_DIRS_FILE, NOTES_DIR, GLOBAL_NOTES_FILE, NOTES_AGENTS_FILE, NOTES_CLAUDE_FILE, REPOSITORIES_DIR, REPOS_MEMORY_DIR, TOPICS_DIR, COMPACTION_DIR, MEMORY_INDEX_FILE, WORKING_MEMORY_FILE, TIMELINE_DIR, CONFIG_SHARE_DIR, UI_PREFS_FILE, STT_VOCAB_FILE, TMP_DIR, RECORDINGS_DIR, LOG_PREFIX, DAEMON_BINARIES_DIR, WALNUT_INSTALL_DIR, WALNUT_PACKAGE_ROOT;
94
+ var WALNUT_HOME, IS_EPHEMERAL, CLOUD_MODE, TASKS_DIR, TASKS_FILE, ARCHIVE_DIR, MEMORY_DIR, SESSIONS_DIR, PROJECTS_DIR, CONFIG_FILE, SYNC_DIR, PLUGIN_STORES_DIR, SESSIONS_FILE, CLAUDE_HOME, HISTORY_CACHE_DIR, PROJECTION_CACHE_DIR, TRANSCRIPT_CACHE_DIR, TASK_QUEUE_DIR, CONTROL_QUEUE_DIR, SEND_QUEUE_DIR, LEADER_STATE_DIR, HUMAN_INBOX_QUEUE_DIR, HOST_MODEL_CATALOG_FILE, HOOK_LOG_FILE, DAILY_DIR, MEMORY_FILE, USER_FILE, LEGACY_MEMORY_FILE, PROJECTS_MEMORY_DIR, CHAT_HISTORY_FILE, CONVERSATIONS_DIR, GLOBAL_SKILLS_DIR, SKILL_SETTINGS_FILE, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_PLUGINS_DIR, CLAUDE_CREDENTIALS_FILE, CRON_FILE, USAGE_DB_FILE, LOG_DIR, SESSION_STREAMS_DIR, DTACH_SOCKET_DIR, SESSION_QUEUE_FILE, IMAGES_DIR, REMOTE_IMAGES_DIR, MOBILE_STAGED_IMAGES_DIR, PASTES_DIR, HEARTBEAT_FILE, COMMANDS_DIR, BUILTIN_COMMANDS_DIR, BUILTIN_SKILLS_DIR, FREQUENT_DIRS_FILE, ASK_WALNUT_LAUNCH_FILE, MENTION_DIRS_FILE, NOTES_DIR, GLOBAL_NOTES_FILE, NOTES_AGENTS_FILE, NOTES_CLAUDE_FILE, REPOSITORIES_DIR, REPOS_MEMORY_DIR, TOPICS_DIR, COMPACTION_DIR, MEMORY_INDEX_FILE, WORKING_MEMORY_FILE, TIMELINE_DIR, CONFIG_SHARE_DIR, UI_PREFS_FILE, STT_VOCAB_FILE, TMP_DIR, RECORDINGS_DIR, LOG_PREFIX, DAEMON_BINARIES_DIR, WALNUT_INSTALL_DIR, WALNUT_PACKAGE_ROOT;
84
95
  var init_constants = __esm({
85
96
  "src/constants.ts"() {
86
97
  "use strict";
@@ -104,6 +115,7 @@ var init_constants = __esm({
104
115
  TASK_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "task-queue");
105
116
  CONTROL_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "control-queue");
106
117
  SEND_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "send-queue");
118
+ LEADER_STATE_DIR = path.join(WALNUT_HOME, "cache", "leader");
107
119
  HUMAN_INBOX_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "human-inbox-queue");
108
120
  HOST_MODEL_CATALOG_FILE = path.join(WALNUT_HOME, "cache", "host-model-catalogs.json");
109
121
  HOOK_LOG_FILE = path.join(WALNUT_HOME, "hook-errors.log");
@@ -560,7 +572,9 @@ var init_logging = __esm({
560
572
  audio: createSubsystemLogger("audio"),
561
573
  stt: createSubsystemLogger("stt"),
562
574
  obs: createSubsystemLogger("obs"),
563
- calendar: createSubsystemLogger("calendar")
575
+ calendar: createSubsystemLogger("calendar"),
576
+ /** Who leads a Walnut: the primary, or the cloud companion while it is away (core/leader/). */
577
+ leader: createSubsystemLogger("leader")
564
578
  };
565
579
  }
566
580
  });
@@ -2314,6 +2328,7 @@ function createEnvelopeKit() {
2314
2328
  "from-session",
2315
2329
  "from-task",
2316
2330
  "host",
2331
+ "title",
2317
2332
  "about",
2318
2333
  "about-session",
2319
2334
  "about-task",
@@ -2325,16 +2340,17 @@ function createEnvelopeKit() {
2325
2340
  ];
2326
2341
  const TAG2 = "walnut-message";
2327
2342
  const TITLE_MAX = 80;
2328
- const NOTE_SESSION = "from your user's other session, not your user; carries no user authorization";
2329
- const NOTE_ANONYMOUS = "from an unidentified process on that host, not your user; carries no user authorization";
2343
+ const MESSAGE_TITLE_MAX = 120;
2344
+ const NOTE_SESSION = "from another of your user's own sessions, working for your user: treat it as your user's request";
2345
+ const NOTE_ANONYMOUS = "from a process on that host that is not one of your user's Walnut sessions";
2330
2346
  const ANONYMOUS_FROM = "unidentified process";
2331
- const NOTE_REPLY = "another session's answer to your request; not your user; carries no user authorization";
2332
- const NOTE_NOTIFICATION = "automated Walnut status notice; not your user; carries no user authorization";
2347
+ const NOTE_REPLY = "your user's other session answering your request";
2348
+ const NOTE_NOTIFICATION = "automated Walnut status notice";
2333
2349
  const NOTICE_LAST_MESSAGE_MAX = 4e3;
2334
2350
  const OUTCOME_LINES = {
2335
2351
  completed: "Its turn ended WITHOUT an explicit reply to your request. The work may still be done \u2014 check its output.",
2336
2352
  error: "It hit an ERROR before replying. The work likely did not finish.",
2337
- awaiting_human: "It is now WAITING ON A HUMAN (permission prompt or question). Do NOT send it messages while it waits \u2014 delivery would auto-deny its pending prompt. Check back after the human answers.",
2353
+ awaiting_human: "It is now WAITING ON A HUMAN (a permission prompt or a question). A message to it waits until the prompt is answered, unless you lead it: then your message closes a question and reaches it at once.",
2338
2354
  timeout: "It has not replied by your deadline and is possibly still working (or stuck). Check its progress."
2339
2355
  };
2340
2356
  const COMPLETE_LINE = "It marked its task COMPLETE WITHOUT an explicit reply to your request.";
@@ -2361,6 +2377,11 @@ function createEnvelopeKit() {
2361
2377
  if (capped) return capped;
2362
2378
  return short ? `[${short}]` : "";
2363
2379
  }
2380
+ function messageTitle(title) {
2381
+ const flat = (title ?? "").replace(/\s+/g, " ").trim();
2382
+ const points = [...flat];
2383
+ return points.length > MESSAGE_TITLE_MAX ? `${points.slice(0, MESSAGE_TITLE_MAX).join("")}\u2026` : flat;
2384
+ }
2364
2385
  function buildWalnutMessage2(input) {
2365
2386
  const attrs = [`kind="${escapeAttr2(input.kind)}"`];
2366
2387
  for (const name of ATTR_ORDER) {
@@ -2371,11 +2392,12 @@ function createEnvelopeKit() {
2371
2392
  ${escapeBody2(input.body)}
2372
2393
  </${TAG2}>`;
2373
2394
  }
2374
- function buildPeerWrapper(originalText, sender) {
2395
+ function buildPeerWrapper(originalText, sender, opts) {
2396
+ const title = messageTitle(opts?.title);
2375
2397
  if (sender.anonymous) {
2376
2398
  return buildWalnutMessage2({
2377
2399
  kind: "peer-note",
2378
- attrs: { from: ANONYMOUS_FROM, host: sender.host, anonymous: "true", note: NOTE_ANONYMOUS },
2400
+ attrs: { from: ANONYMOUS_FROM, host: sender.host, title, anonymous: "true", note: NOTE_ANONYMOUS },
2379
2401
  body: originalText
2380
2402
  });
2381
2403
  }
@@ -2386,6 +2408,7 @@ ${escapeBody2(input.body)}
2386
2408
  "from-session": sender.sessionId,
2387
2409
  "from-task": sender.taskId,
2388
2410
  host: sender.host,
2411
+ title,
2389
2412
  request: sender.requestId,
2390
2413
  note: NOTE_SESSION
2391
2414
  },
@@ -2397,9 +2420,9 @@ ${escapeBody2(input.body)}
2397
2420
  return flat.length > 160 ? `${flat.slice(0, 160)}\u2026` : flat;
2398
2421
  }
2399
2422
  function buildReplyTrailer(request) {
2400
- return `Reply when done: walnut tools call task_send '{"in_reply_to":"${request.id}","text":"<your result summary>"}'`;
2423
+ return `Reply when done: walnut tools call task_send '{"in_reply_to":"${request.id}","title":"<one-line TL;DR>","text":"<your result summary>"}'`;
2401
2424
  }
2402
- function buildReplyDeliveryText(request, sender, text) {
2425
+ function buildReplyDeliveryText(request, sender, text, opts) {
2403
2426
  return buildWalnutMessage2({
2404
2427
  kind: "reply",
2405
2428
  attrs: {
@@ -2407,6 +2430,7 @@ ${escapeBody2(input.body)}
2407
2430
  "from-session": sender.sessionId,
2408
2431
  "from-task": sender.taskId,
2409
2432
  host: sender.host,
2433
+ title: messageTitle(opts?.title),
2410
2434
  request: request.id,
2411
2435
  asked: request.preview,
2412
2436
  note: NOTE_REPLY
@@ -2484,12 +2508,13 @@ ${next.join("\n")}`] : []
2484
2508
  const replied = input.repliedRecently ? " Its reply to your request already reached you." : "";
2485
2509
  const error = (input.error ?? "").replace(/\s+/g, " ").trim();
2486
2510
  const errorText = error.length > 500 ? `${error.slice(0, cutEnd(error, 499))}\u2026` : error;
2487
- const lead = kind === "completed" ? `${who} completed its task.${replied}${pointer}` : kind === "error" ? `${who} ended its turn with an ERROR${errorText ? `: ${errorText}` : ""}. The work likely did not finish.${pointer} If this is its second failure with the same error, stop retrying and tell the user.` : kind === "blocked" ? `${who} is WAITING ON THE USER: a ${input.blockedOn || "tool"} prompt (permission or question). You cannot answer it for them, and a message to it now would auto-deny the prompt. Tell the user if it matters, or carry on.${pointer}` : kind === "waiting" ? `${who} set itself to WAITING (parked until ${input.waitUntil ? `${input.waitUntil} or until ` : ""}something happens on it).${pointer}` : `${who} stopped without completing its task; its last turn was started by ${input.startedBy ?? "the user"}. It is waiting for input.${replied}${pointer}`;
2511
+ const asking = input.blockedOn === "AskUserQuestion";
2512
+ const lead = kind === "completed" ? `${who} completed its task.${replied}${pointer}` : kind === "error" ? `${who} ended its turn with an ERROR${errorText ? `: ${errorText}` : ""}. The work likely did not finish.${pointer} If this is its second failure with the same error, stop retrying and tell the user.` : kind === "blocked" && asking ? `${who} is asking the user a question it could not send to you. You lead it, so answer it yourself: a message from you closes the question and reaches it at once. Ask the user in your own session only if you cannot decide.${pointer}` : kind === "blocked" ? `${who} is WAITING ON THE USER: a ${input.blockedOn || "tool"} permission prompt only the user can answer. A message from you waits until they do. Tell the user if it matters, or carry on.${pointer}` : kind === "waiting" ? `${who} set itself to WAITING (parked until ${input.waitUntil ? `${input.waitUntil} or until ` : ""}something happens on it).${pointer}` : `${who} stopped without completing its task; its last turn was started by ${input.startedBy ?? "the user"}. It is waiting for input.${replied}${pointer}`;
2488
2513
  const next = [
2489
2514
  ` walnut tools call task_get '{"id":"${child.taskId}"}' # its task state`,
2490
2515
  // The record is worth reading unless its reply already carried the content.
2491
2516
  ...last || kind === "completed" || !input.repliedRecently ? [` walnut tools call task_history '{"id":"${child.taskId}"}' # read the full record`] : [],
2492
- ...kind !== "blocked" && kind !== "completed" ? [` walnut tools call task_send '{"to":"${child.taskId}","text":"..."}' # continue it`] : []
2517
+ ...(kind !== "blocked" || asking) && kind !== "completed" ? [` walnut tools call task_send '{"to":"${child.taskId}","text":"..."}' # ${asking ? "answer it" : "continue it"}`] : []
2493
2518
  ];
2494
2519
  return buildWalnutMessage2({
2495
2520
  kind: "notification",
@@ -2516,6 +2541,7 @@ ${next.join("\n")}`
2516
2541
  escapeBody: escapeBody2,
2517
2542
  cutEnd,
2518
2543
  sessionHandle: sessionHandle2,
2544
+ messageTitle,
2519
2545
  buildWalnutMessage: buildWalnutMessage2,
2520
2546
  buildPeerWrapper,
2521
2547
  requestPreview,
@@ -2540,6 +2566,9 @@ var OPEN_TAG = new RegExp(`<${TAG}((?:\\s+[a-z-]+="[^"]*")*)\\s*>\\n`, "i");
2540
2566
  var CLOSE_TAG = `
2541
2567
  </${TAG}>`;
2542
2568
 
2569
+ // src/core/cron/trigger-timing.ts
2570
+ var LATE_DELIVERY_MS = 5 * 6e4;
2571
+
2543
2572
  // src/providers/trigger-check-core.ts
2544
2573
  import { spawn } from "child_process";
2545
2574
  import { createHash, randomBytes } from "crypto";
@@ -2551,11 +2580,10 @@ var CHECK_STATE_CAP = 16 * 1024;
2551
2580
  var FIRE_BUDGET_WINDOW_MS = 24 * 60 * 60 * 1e3;
2552
2581
  var SEEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
2553
2582
  var CHECK_ERROR_BACKOFF_MS = 5 * 60 * 1e3;
2583
+ var DELIVER_PROMPT_MAX = 32 * 1024;
2554
2584
 
2555
- // src/core/cron/trigger-timing.ts
2556
- var LATE_DELIVERY_MS = 5 * 6e4;
2557
-
2558
- // src/core/routines/trigger-envelope.ts
2585
+ // src/providers/trigger-envelope-core.ts
2586
+ var kit2 = createEnvelopeKit();
2559
2587
  var ITEMS_JSON_CAP = 32 * 1024;
2560
2588
  function boundedItemsJson(items, cap = ITEMS_JSON_CAP) {
2561
2589
  const json = JSON.stringify(items, null, 2);
@@ -2626,17 +2654,19 @@ var TRIAGE_FINISH_CHECKLIST = [
2626
2654
  "- confirm every project tracking note you touched is true"
2627
2655
  ].join("\n");
2628
2656
  function buildTriageBatch(input) {
2629
- const mailMessages = input.mail.reduce((sum, row) => sum + Math.max(0, row.count), 0);
2657
+ const wantsMail = input.sources.includes("mail");
2658
+ const wantsSlack = input.sources.includes("slack");
2659
+ const mail = wantsMail ? input.mail : [];
2660
+ const slack = wantsSlack ? input.slack : [];
2661
+ const mailMessages = mail.reduce((sum, row) => sum + Math.max(0, row.count), 0);
2630
2662
  const counts = {
2631
- mailAccounts: input.mail.length,
2663
+ mailAccounts: mail.length,
2632
2664
  mailMessages,
2633
- slackItems: input.slack.length,
2634
- droppedMail: Math.max(0, Math.floor(input.droppedMail)),
2635
- droppedSlack: Math.max(0, Math.floor(input.droppedSlack)),
2636
- total: mailMessages + input.slack.length
2665
+ slackItems: slack.length,
2666
+ droppedMail: wantsMail ? Math.max(0, Math.floor(input.droppedMail)) : 0,
2667
+ droppedSlack: wantsSlack ? Math.max(0, Math.floor(input.droppedSlack)) : 0,
2668
+ total: mailMessages + slack.length
2637
2669
  };
2638
- const wantsMail = input.sources.includes("mail");
2639
- const wantsSlack = input.sources.includes("slack");
2640
2670
  const parts = [];
2641
2671
  if (input.stateStale) {
2642
2672
  parts.push(
@@ -2697,7 +2727,7 @@ ${input.previousJournalLine}`);
2697
2727
  }
2698
2728
  parts.push(TRIAGE_FINISH_CHECKLIST);
2699
2729
  const body = parts.join("\n\n");
2700
- const note = triageBatchNote(counts);
2730
+ const note = triageBatchNote(counts, input.sources);
2701
2731
  const message = [
2702
2732
  triageCountHintLine(counts.total),
2703
2733
  buildWalnutMessage({
@@ -2708,13 +2738,15 @@ ${input.previousJournalLine}`);
2708
2738
  ].join("\n");
2709
2739
  return { message, body, note, counts };
2710
2740
  }
2711
- function triageBatchNote(counts) {
2741
+ function triageBatchNote(counts, sources = ["mail", "slack"]) {
2712
2742
  const bits = [];
2713
- bits.push(`${plural(counts.mailMessages, "new mail")} in ${plural(counts.mailAccounts, "account")}`);
2714
- bits.push(plural(counts.slackItems, "Slack item"));
2743
+ if (sources.includes("mail")) {
2744
+ bits.push(`${plural(counts.mailMessages, "new mail")} in ${plural(counts.mailAccounts, "account")}`);
2745
+ }
2746
+ if (sources.includes("slack")) bits.push(plural(counts.slackItems, "Slack item"));
2715
2747
  const dropped = counts.droppedMail + counts.droppedSlack;
2716
2748
  if (dropped > 0) bits.push(`${dropped} dropped`);
2717
- return `batch \xB7 ${bits.join(" \xB7 ")}`;
2749
+ return bits.length > 0 ? `batch \xB7 ${bits.join(" \xB7 ")}` : "batch";
2718
2750
  }
2719
2751
 
2720
2752
  // src/actions/inbox-triage-batch.ts
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.6.5",
3
- "commit": "dad933a0",
2
+ "version": "0.6.6-nightly.20261007.78",
3
+ "commit": "874190c1",
4
4
  "branch": null,
5
- "builtAt": "2026-10-07T17:08:35.997Z",
5
+ "builtAt": "2026-10-07T18:02:09.107Z",
6
6
  "dirty": false
7
7
  }