claude-threads 1.36.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12676,6 +12676,29 @@ var init_emoji = __esm(() => {
12676
12676
  ALLOW_ALL_EMOJIS = ["white_check_mark", "heavy_check_mark"];
12677
12677
  });
12678
12678
 
12679
+ // src/config/types.ts
12680
+ var STDIO_KEYS, REMOTE_KEYS;
12681
+ var init_types = __esm(() => {
12682
+ STDIO_KEYS = new Set(["type", "command", "args", "env"]);
12683
+ REMOTE_KEYS = new Set(["type", "url", "headers"]);
12684
+ });
12685
+
12686
+ // src/utils/spawn.ts
12687
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
12688
+ function addWindowsShell(options) {
12689
+ if (isWindows && options.shell === undefined) {
12690
+ return { ...options, shell: true };
12691
+ }
12692
+ return options;
12693
+ }
12694
+ function crossSpawn(command, args, options) {
12695
+ return nodeSpawn(command, args, addWindowsShell(options ?? {}));
12696
+ }
12697
+ var isWindows;
12698
+ var init_spawn = __esm(() => {
12699
+ isWindows = process.platform === "win32";
12700
+ });
12701
+
12679
12702
  // src/utils/logger.ts
12680
12703
  function createLogger(component, useStderr = false, sessionId) {
12681
12704
  const isDebug = () => process.env.DEBUG === "1";
@@ -14593,7 +14616,7 @@ var require_semver2 = __commonJS(function(exports, module) {
14593
14616
 
14594
14617
  // src/claude/version-check.ts
14595
14618
  import { execSync } from "child_process";
14596
- import { existsSync as existsSync2 } from "fs";
14619
+ import { existsSync } from "fs";
14597
14620
  import { join } from "path";
14598
14621
  function tryClaudeVersion(claudePath) {
14599
14622
  try {
@@ -14646,7 +14669,7 @@ function getClaudePath() {
14646
14669
  return whichResult;
14647
14670
  }
14648
14671
  for (const path of COMMON_CLAUDE_PATHS) {
14649
- if (existsSync2(path)) {
14672
+ if (existsSync(path)) {
14650
14673
  const result = tryClaudeVersion(path);
14651
14674
  if (!result.error) {
14652
14675
  discoveredClaudePath = path;
@@ -14674,29 +14697,6 @@ var init_version_check = __esm(() => {
14674
14697
  ];
14675
14698
  });
14676
14699
 
14677
- // src/utils/spawn.ts
14678
- import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
14679
- function addWindowsShell(options) {
14680
- if (isWindows && options.shell === undefined) {
14681
- return { ...options, shell: true };
14682
- }
14683
- return options;
14684
- }
14685
- function crossSpawn(command, args, options) {
14686
- return nodeSpawn(command, args, addWindowsShell(options ?? {}));
14687
- }
14688
- var isWindows;
14689
- var init_spawn = __esm(() => {
14690
- isWindows = process.platform === "win32";
14691
- });
14692
-
14693
- // src/config/types.ts
14694
- var STDIO_KEYS, REMOTE_KEYS;
14695
- var init_types = __esm(() => {
14696
- STDIO_KEYS = new Set(["type", "command", "args", "env"]);
14697
- REMOTE_KEYS = new Set(["type", "url", "headers"]);
14698
- });
14699
-
14700
14700
  // src/mcp/outbound-env.ts
14701
14701
  var OUTBOUND_ENV;
14702
14702
  var init_outbound_env = __esm(() => {
@@ -14759,7 +14759,7 @@ function buildClaudeChildEnv(parentEnv, account, opts) {
14759
14759
  }
14760
14760
  return env;
14761
14761
  }
14762
- var log14, STDERR_AGGREGATE_SOFT_CAP;
14762
+ var log, STDERR_AGGREGATE_SOFT_CAP;
14763
14763
  var init_cli = __esm(() => {
14764
14764
  init_types();
14765
14765
  init_spawn();
@@ -14768,7 +14768,7 @@ var init_cli = __esm(() => {
14768
14768
  init_outbound_env();
14769
14769
  init_agent_features_env();
14770
14770
  init_rate_limit_detector();
14771
- log14 = createLogger("claude");
14771
+ log = createLogger("claude");
14772
14772
  STDERR_AGGREGATE_SOFT_CAP = 10 * 1024 * 1024;
14773
14773
  });
14774
14774
 
@@ -15007,7 +15007,7 @@ var init_render = __esm(() => {
15007
15007
  });
15008
15008
 
15009
15009
  // src/usage/index.ts
15010
- import { homedir as homedir5 } from "os";
15010
+ import { homedir as homedir2 } from "os";
15011
15011
  import path4 from "path";
15012
15012
  function toLimits(usage) {
15013
15013
  const limits = [
@@ -15057,7 +15057,7 @@ async function collectUsage(options) {
15057
15057
  }
15058
15058
  return results;
15059
15059
  }
15060
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? path4.join(homedir5(), ".claude");
15060
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? path4.join(homedir2(), ".claude");
15061
15061
  return [await readSeat(profileNameFor(configDir), configDir)];
15062
15062
  }
15063
15063
  var USAGE_PROBE_TIMEOUT_MS = 1e4;
@@ -33912,9 +33912,11 @@ function formatToolForPermission(toolName, input, formatter, options = {}) {
33912
33912
  });
33913
33913
  return result.permissionText ?? result.display ?? toolName;
33914
33914
  }
33915
+ // src/operations/transformer.ts
33916
+ init_cli();
33915
33917
  // src/session/lifecycle-fsm.ts
33916
33918
  init_logger();
33917
- var log = createLogger("fsm");
33919
+ var log2 = createLogger("fsm");
33918
33920
  var ALLOWED_TRANSITIONS = {
33919
33921
  starting: new Set(["active", "paused", "interrupted", "cancelling", "restarting"]),
33920
33922
  active: new Set([
@@ -33956,7 +33958,7 @@ function createSessionLog(baseLog) {
33956
33958
  init_logger();
33957
33959
 
33958
33960
  // src/version.ts
33959
- import { readFileSync, existsSync } from "fs";
33961
+ import { readFileSync, existsSync as existsSync2 } from "fs";
33960
33962
  import { dirname, resolve } from "path";
33961
33963
  import { fileURLToPath } from "url";
33962
33964
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
@@ -33967,7 +33969,7 @@ function loadPackageJson() {
33967
33969
  resolve(process.cwd(), "package.json")
33968
33970
  ];
33969
33971
  for (const candidate of candidates) {
33970
- if (existsSync(candidate)) {
33972
+ if (existsSync2(candidate)) {
33971
33973
  try {
33972
33974
  const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
33973
33975
  if (pkg.name === "claude-threads") {
@@ -33995,23 +33997,34 @@ function formatShortId(id) {
33995
33997
  }
33996
33998
 
33997
33999
  // src/utils/error-handler/index.ts
33998
- var log2 = createLogger("error");
34000
+ var log3 = createLogger("error");
33999
34001
 
34000
34002
  // src/operations/post-helpers/index.ts
34001
34003
  init_emoji();
34002
34004
 
34003
34005
  // src/git/worktree.ts
34004
34006
  init_spawn();
34007
+
34008
+ // src/utils/state-home.ts
34009
+ import { homedir } from "os";
34010
+ import { resolve as resolve2 } from "path";
34011
+ function stateHome() {
34012
+ return resolve2(process.env.CLAUDE_THREADS_HOME || homedir());
34013
+ }
34014
+
34015
+ // src/git/worktree.ts
34005
34016
  import * as path from "path";
34006
34017
  init_logger();
34007
- import { homedir } from "os";
34008
- var log3 = createLogger("git-wt");
34009
- var WORKTREES_DIR = path.join(homedir(), ".claude-threads", "worktrees");
34010
- var METADATA_STORE_PATH = path.join(homedir(), ".claude-threads", "worktree-metadata.json");
34018
+ var log4 = createLogger("git-wt");
34019
+ var WORKTREES_DIR = path.join(stateHome(), ".claude-threads", "worktrees");
34020
+ var METADATA_STORE_PATH = path.join(stateHome(), ".claude-threads", "worktree-metadata.json");
34011
34021
 
34012
34022
  // src/operations/post-helpers/index.ts
34013
- var log4 = createLogger("helpers");
34014
- var sessionLog = createSessionLog(log4);
34023
+ var log5 = createLogger("helpers");
34024
+ var sessionLog = createSessionLog(log5);
34025
+
34026
+ // src/operations/message-manager.ts
34027
+ init_types();
34015
34028
 
34016
34029
  // src/operations/task-tracker.ts
34017
34030
  var VALID_STATUSES = new Set(["pending", "in_progress", "completed"]);
@@ -34144,7 +34157,7 @@ init_emoji();
34144
34157
 
34145
34158
  // src/persistence/audit-log.ts
34146
34159
  init_logger();
34147
- var log5 = createLogger("audit");
34160
+ var log6 = createLogger("audit");
34148
34161
  var enabledPlatforms = new Set;
34149
34162
  var preparedDirs = new Set;
34150
34163
  var openFds = new Map;
@@ -34157,13 +34170,13 @@ init_emoji();
34157
34170
  // src/operations/executors/worktree-prompt.ts
34158
34171
  init_emoji();
34159
34172
  init_logger();
34160
- var log6 = createLogger("wt-prompt");
34173
+ var log7 = createLogger("wt-prompt");
34161
34174
  // src/operations/message-manager.ts
34162
34175
  init_logger();
34163
34176
 
34164
34177
  // src/transcription/elevenlabs.ts
34165
34178
  init_logger();
34166
- var log7 = createLogger("transcribe");
34179
+ var log8 = createLogger("transcribe");
34167
34180
 
34168
34181
  // src/transcription/types.ts
34169
34182
  var AUDIO_EXTENSIONS = new Set(["m4a", "mp3", "ogg", "opus", "wav", "aac", "flac", "webm"]);
@@ -34192,12 +34205,11 @@ function formatBytes(bytes) {
34192
34205
  }
34193
34206
 
34194
34207
  // src/operations/streaming/handler.ts
34195
- var log8 = createLogger("streaming");
34208
+ var log9 = createLogger("streaming");
34196
34209
  // src/operations/message-manager.ts
34197
- var log9 = createLogger("msg-mgr");
34210
+ var log10 = createLogger("msg-mgr");
34198
34211
  // src/persistence/session-store.ts
34199
34212
  init_logger();
34200
- import { homedir as homedir2 } from "os";
34201
34213
  import { join as join3 } from "path";
34202
34214
 
34203
34215
  // src/sponsor.ts
@@ -34208,13 +34220,12 @@ function formatSponsorFooter(formatter) {
34208
34220
  }
34209
34221
 
34210
34222
  // src/persistence/session-store.ts
34211
- var log10 = createLogger("persist");
34212
- var DEFAULT_CONFIG_DIR = join3(homedir2(), ".config", "claude-threads");
34223
+ var log11 = createLogger("persist");
34224
+ var DEFAULT_CONFIG_DIR = join3(stateHome(), ".config", "claude-threads");
34213
34225
  var DEFAULT_SESSIONS_FILE = join3(DEFAULT_CONFIG_DIR, "sessions.json");
34214
34226
 
34215
34227
  // src/config/index.ts
34216
- import { resolve as resolve3, dirname as dirname3 } from "path";
34217
- import { homedir as homedir3 } from "os";
34228
+ import { resolve as resolve4, dirname as dirname3 } from "path";
34218
34229
 
34219
34230
  // node_modules/js-yaml/dist/js-yaml.mjs
34220
34231
  function getDefaultExportFromCjs(x) {
@@ -37356,7 +37367,7 @@ init_types();
37356
37367
  init_types();
37357
37368
 
37358
37369
  // src/config/index.ts
37359
- var CONFIG_PATH = resolve3(homedir3(), ".config", "claude-threads", "config.yaml");
37370
+ var CONFIG_PATH = resolve4(stateHome(), ".config", "claude-threads", "config.yaml");
37360
37371
 
37361
37372
  // src/utils/battery.ts
37362
37373
  import { exec } from "child_process";
@@ -37365,13 +37376,13 @@ var execAsync = promisify(exec);
37365
37376
 
37366
37377
  // src/changelog.ts
37367
37378
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
37368
- import { dirname as dirname4, resolve as resolve4 } from "path";
37379
+ import { dirname as dirname4, resolve as resolve5 } from "path";
37369
37380
  import { fileURLToPath as fileURLToPath2 } from "url";
37370
37381
  var __dirname3 = dirname4(fileURLToPath2(import.meta.url));
37371
37382
  function getReleaseNotes(version) {
37372
37383
  const possiblePaths = [
37373
- resolve4(__dirname3, "..", "CHANGELOG.md"),
37374
- resolve4(__dirname3, "..", "..", "CHANGELOG.md")
37384
+ resolve5(__dirname3, "..", "CHANGELOG.md"),
37385
+ resolve5(__dirname3, "..", "..", "CHANGELOG.md")
37375
37386
  ];
37376
37387
  let changelogPath = null;
37377
37388
  for (const p of possiblePaths) {
@@ -37465,7 +37476,7 @@ init_logger();
37465
37476
  // src/utils/keep-alive.ts
37466
37477
  init_logger();
37467
37478
  import { spawn } from "child_process";
37468
- var log11 = createLogger("keepalive");
37479
+ var log12 = createLogger("keepalive");
37469
37480
  function keepAliveSpawnSpec(platform, parentPid) {
37470
37481
  switch (platform) {
37471
37482
  case "darwin":
@@ -37521,7 +37532,7 @@ class KeepAliveManager {
37521
37532
  if (!enabled && this.keepAliveProcess) {
37522
37533
  this.stopKeepAlive();
37523
37534
  }
37524
- log11.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
37535
+ log12.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
37525
37536
  }
37526
37537
  isEnabled() {
37527
37538
  return this.enabled;
@@ -37531,7 +37542,7 @@ class KeepAliveManager {
37531
37542
  }
37532
37543
  sessionStarted() {
37533
37544
  this.activeSessionCount++;
37534
- log11.debug(`Session started (${this.activeSessionCount} active)`);
37545
+ log12.debug(`Session started (${this.activeSessionCount} active)`);
37535
37546
  if (this.activeSessionCount === 1) {
37536
37547
  this.startKeepAlive();
37537
37548
  }
@@ -37540,7 +37551,7 @@ class KeepAliveManager {
37540
37551
  if (this.activeSessionCount > 0) {
37541
37552
  this.activeSessionCount--;
37542
37553
  }
37543
- log11.debug(`Session ended (${this.activeSessionCount} active)`);
37554
+ log12.debug(`Session ended (${this.activeSessionCount} active)`);
37544
37555
  if (this.activeSessionCount === 0) {
37545
37556
  this.stopKeepAlive();
37546
37557
  }
@@ -37554,11 +37565,11 @@ class KeepAliveManager {
37554
37565
  }
37555
37566
  startKeepAlive() {
37556
37567
  if (!this.enabled) {
37557
- log11.debug("Keep-alive disabled, skipping");
37568
+ log12.debug("Keep-alive disabled, skipping");
37558
37569
  return;
37559
37570
  }
37560
37571
  if (this.keepAliveProcess) {
37561
- log11.debug("Keep-alive already running");
37572
+ log12.debug("Keep-alive already running");
37562
37573
  return;
37563
37574
  }
37564
37575
  switch (this.platform) {
@@ -37572,12 +37583,12 @@ class KeepAliveManager {
37572
37583
  this.startWindowsKeepAlive();
37573
37584
  break;
37574
37585
  default:
37575
- log11.warn(`Keep-alive not supported on ${this.platform}`);
37586
+ log12.warn(`Keep-alive not supported on ${this.platform}`);
37576
37587
  }
37577
37588
  }
37578
37589
  stopKeepAlive() {
37579
37590
  if (this.keepAliveProcess) {
37580
- log11.debug("Stopping keep-alive");
37591
+ log12.debug("Stopping keep-alive");
37581
37592
  this.keepAliveProcess.kill();
37582
37593
  this.keepAliveProcess = null;
37583
37594
  }
@@ -37592,18 +37603,18 @@ class KeepAliveManager {
37592
37603
  detached: false
37593
37604
  });
37594
37605
  this.keepAliveProcess.on("error", (err) => {
37595
- log11.error(`Failed to start caffeinate: ${err.message}`);
37606
+ log12.error(`Failed to start caffeinate: ${err.message}`);
37596
37607
  this.keepAliveProcess = null;
37597
37608
  });
37598
37609
  this.keepAliveProcess.on("exit", (code) => {
37599
37610
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
37600
- log11.debug(`caffeinate exited with code ${code}`);
37611
+ log12.debug(`caffeinate exited with code ${code}`);
37601
37612
  }
37602
37613
  this.keepAliveProcess = null;
37603
37614
  });
37604
- log11.info("Sleep prevention active (caffeinate)");
37615
+ log12.info("Sleep prevention active (caffeinate)");
37605
37616
  } catch (err) {
37606
- log11.error(`Failed to start caffeinate: ${err}`);
37617
+ log12.error(`Failed to start caffeinate: ${err}`);
37607
37618
  }
37608
37619
  }
37609
37620
  startLinuxKeepAlive() {
@@ -37616,19 +37627,19 @@ class KeepAliveManager {
37616
37627
  detached: false
37617
37628
  });
37618
37629
  this.keepAliveProcess.on("error", (err) => {
37619
- log11.debug(`systemd-inhibit not available: ${err.message}`);
37630
+ log12.debug(`systemd-inhibit not available: ${err.message}`);
37620
37631
  this.keepAliveProcess = null;
37621
37632
  this.startLinuxKeepAliveFallback();
37622
37633
  });
37623
37634
  this.keepAliveProcess.on("exit", (code) => {
37624
37635
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
37625
- log11.debug(`systemd-inhibit exited with code ${code}`);
37636
+ log12.debug(`systemd-inhibit exited with code ${code}`);
37626
37637
  }
37627
37638
  this.keepAliveProcess = null;
37628
37639
  });
37629
- log11.info("Sleep prevention active (systemd-inhibit)");
37640
+ log12.info("Sleep prevention active (systemd-inhibit)");
37630
37641
  } catch (err) {
37631
- log11.debug(`Failed to start systemd-inhibit: ${err}`);
37642
+ log12.debug(`Failed to start systemd-inhibit: ${err}`);
37632
37643
  this.startLinuxKeepAliveFallback();
37633
37644
  }
37634
37645
  }
@@ -37639,15 +37650,15 @@ class KeepAliveManager {
37639
37650
  detached: false
37640
37651
  });
37641
37652
  this.keepAliveProcess.on("error", (err) => {
37642
- log11.warn(`Linux keep-alive fallback not available: ${err.message}`);
37653
+ log12.warn(`Linux keep-alive fallback not available: ${err.message}`);
37643
37654
  this.keepAliveProcess = null;
37644
37655
  });
37645
37656
  this.keepAliveProcess.on("exit", () => {
37646
37657
  this.keepAliveProcess = null;
37647
37658
  });
37648
- log11.info("Sleep prevention active (xdg-screensaver)");
37659
+ log12.info("Sleep prevention active (xdg-screensaver)");
37649
37660
  } catch (err) {
37650
- log11.warn(`Linux keep-alive not available: ${err}`);
37661
+ log12.warn(`Linux keep-alive not available: ${err}`);
37651
37662
  }
37652
37663
  }
37653
37664
  startWindowsKeepAlive() {
@@ -37659,25 +37670,25 @@ class KeepAliveManager {
37659
37670
  windowsHide: true
37660
37671
  });
37661
37672
  this.keepAliveProcess.on("error", (err) => {
37662
- log11.warn(`Windows keep-alive not available: ${err.message}`);
37673
+ log12.warn(`Windows keep-alive not available: ${err.message}`);
37663
37674
  this.keepAliveProcess = null;
37664
37675
  });
37665
37676
  this.keepAliveProcess.on("exit", (code) => {
37666
37677
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
37667
- log11.debug(`PowerShell keep-alive exited with code ${code}`);
37678
+ log12.debug(`PowerShell keep-alive exited with code ${code}`);
37668
37679
  }
37669
37680
  this.keepAliveProcess = null;
37670
37681
  });
37671
- log11.info("Sleep prevention active (SetThreadExecutionState)");
37682
+ log12.info("Sleep prevention active (SetThreadExecutionState)");
37672
37683
  } catch (err) {
37673
- log11.warn(`Windows keep-alive not available: ${err}`);
37684
+ log12.warn(`Windows keep-alive not available: ${err}`);
37674
37685
  }
37675
37686
  }
37676
37687
  }
37677
37688
  var keepAlive = new KeepAliveManager;
37678
37689
 
37679
37690
  // src/operations/sticky-message/handler.ts
37680
- var log12 = createLogger("sticky");
37691
+ var log13 = createLogger("sticky");
37681
37692
  var botStartedAt = new Date;
37682
37693
  var stickyPostIds = new Map;
37683
37694
  var needsBump = new Map;
@@ -37920,10 +37931,9 @@ init_version_check();
37920
37931
 
37921
37932
  // src/persistence/thread-logger.ts
37922
37933
  init_logger();
37923
- import { homedir as homedir4 } from "os";
37924
37934
  import { join as join4, dirname as dirname5 } from "path";
37925
- var log13 = createLogger("thread-log");
37926
- var LOGS_BASE_DIR = join4(homedir4(), ".claude-threads", "logs");
37935
+ var log14 = createLogger("thread-log");
37936
+ var LOGS_BASE_DIR = join4(stateHome(), ".claude-threads", "logs");
37927
37937
 
37928
37938
  // src/operations/bug-report/handler.ts
37929
37939
  var piiRedactor = new Redactor({ aggressive: true });
@@ -38830,11 +38840,10 @@ var sessionLog3 = createSessionLog(log21);
38830
38840
  var contextPromptTimeouts = new Map;
38831
38841
  var contextPromptFiles = new Map;
38832
38842
  // src/memory/store.ts
38833
- import { homedir as homedir6 } from "os";
38834
38843
  import { basename as basename3, dirname as dirname6, join as join5, sep as sep2 } from "path";
38835
38844
  init_logger();
38836
38845
  var log22 = createLogger("memory");
38837
- var DEFAULT_ROOT = join5(homedir6(), ".config", "claude-threads", "memory");
38846
+ var DEFAULT_ROOT = join5(stateHome(), ".config", "claude-threads", "memory");
38838
38847
  var CHANNEL_BLOCK_MAX_BYTES = 25 * 1024;
38839
38848
 
38840
38849
  // src/memory/distiller.ts
@@ -38849,9 +38858,8 @@ import { join as join7 } from "path";
38849
38858
  init_logger();
38850
38859
 
38851
38860
  // src/persistence/platform-list-store.ts
38852
- import { homedir as homedir7 } from "os";
38853
38861
  import { join as join6 } from "path";
38854
- var STORES_CONFIG_DIR = join6(homedir7(), ".config", "claude-threads");
38862
+ var STORES_CONFIG_DIR = join6(stateHome(), ".config", "claude-threads");
38855
38863
 
38856
38864
  // src/persistence/routines-store.ts
38857
38865
  var log24 = createLogger("routines");
@@ -38904,11 +38912,10 @@ init_logger();
38904
38912
  init_quick_query();
38905
38913
 
38906
38914
  // src/persistence/github-emails-store.ts
38907
- import { homedir as homedir8 } from "os";
38908
38915
  import { join as join9 } from "path";
38909
38916
  init_logger();
38910
38917
  var log32 = createLogger("gh-emails");
38911
- var DEFAULT_CONFIG_DIR2 = join9(homedir8(), ".config", "claude-threads");
38918
+ var DEFAULT_CONFIG_DIR2 = join9(stateHome(), ".config", "claude-threads");
38912
38919
  var DEFAULT_FILE3 = join9(DEFAULT_CONFIG_DIR2, "github-emails.yaml");
38913
38920
 
38914
38921
  // src/operations/commands/handler.ts
@@ -171,6 +171,7 @@ What happens: every `audio/*` attachment (or a file with an audio extension when
171
171
  | `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
172
172
  | `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
173
173
  | `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
174
+ | `lifecycle` | No | Session status posts — idle warning, timeout, pause, resume, and the shutdown notice a deploy leaves in each open thread: `full` (default, all of them) / `minimal` (drops the idle warning, which predicts a timeout the next message would undo anyway) / `hidden` (none). Editing a status post the thread already has is not suppressed at any level: it adds no post and no notification, and leaving a stale "session idle" up across a restart would be worse. ⚠️ An **abnormal exit** (`[Exited: <code>]`, non-zero only) posts at every level including `hidden`: it is a failure report, and silencing it would make a session that died look exactly like one that finished. Note that `hidden` also removes the post a 🔄 reaction resumes from — sending a message in the thread still resumes it, and the channel sticky says so. |
174
175
  | `directChannelMode` | No | Direct channel mode: the whole channel is one session, and the bot replies with top-level channel posts instead of thread replies. `true` for defaults, or an options object (`respondTo`). See [Direct Channel Mode](#direct-channel-mode). |
175
176
  | `approvals` | No | Who may answer tool-permission prompts and other reaction gates: `owner` (session participants) or `all_users` (everyone on `allowedUsers`). Unset keeps the historical default per mode — `all_users` for thread sessions, `owner` for direct channel mode. See [Approvals](#approvals). |
176
177
  | `ackReaction` | No | Read receipt: react to every accepted message (session start, follow-up, resume) the instant it is accepted, before Claude produces output. `true` uses 👀 (`eyes`), a string names a custom emoji. Persistent, unlike the typing indicator — useful in busy channels and for messages queued behind an in-flight session start. The receipt means *accepted*, not *delivered*: a later failure (capacity limit, Claude not coming up) is still reported by its own post. `!commands` are not acked — they have their own immediate feedback, and neither are messages accepted through the message-approval flow (an authorized user approving a non-participant's message) — there the approval reaction is already the visible signal. Note: in direct channel mode this is one reaction API call per accepted message. Default off. |
@@ -195,6 +196,7 @@ What happens: every `audio/*` attachment (or a file with an audio extension when
195
196
  | `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
196
197
  | `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
197
198
  | `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
199
+ | `lifecycle` | No | Session status posts — idle warning, timeout, pause, resume, and the shutdown notice a deploy leaves in each open thread: `full` (default, all of them) / `minimal` (drops the idle warning, which predicts a timeout the next message would undo anyway) / `hidden` (none). Editing a status post the thread already has is not suppressed at any level: it adds no post and no notification, and leaving a stale "session idle" up across a restart would be worse. ⚠️ An **abnormal exit** (`[Exited: <code>]`, non-zero only) posts at every level including `hidden`: it is a failure report, and silencing it would make a session that died look exactly like one that finished. Note that `hidden` also removes the post a 🔄 reaction resumes from — sending a message in the thread still resumes it, and the channel sticky says so. |
198
200
  | `directChannelMode` | No | Direct channel mode: the whole channel is one session, and the bot replies with top-level channel posts instead of thread replies. `true` for defaults, or an options object (`respondTo`). See [Direct Channel Mode](#direct-channel-mode). |
199
201
  | `approvals` | No | Who may answer tool-permission prompts and other reaction gates: `owner` (session participants) or `all_users` (everyone on `allowedUsers`). Unset keeps the historical default per mode — `all_users` for thread sessions, `owner` for direct channel mode. See [Approvals](#approvals). |
200
202
  | `ackReaction` | No | Read receipt: react to every accepted message (session start, follow-up, resume) the instant it is accepted, before Claude produces output. `true` uses 👀 (`eyes`), a string names a custom emoji. Persistent, unlike the typing indicator — useful in busy channels and for messages queued behind an in-flight session start. The receipt means *accepted*, not *delivered*: a later failure (capacity limit, Claude not coming up) is still reported by its own post. `!commands` are not acked — they have their own immediate feedback, and neither are messages accepted through the message-approval flow (an authorized user approving a non-participant's message) — there the approval reaction is already the visible signal. Note: in direct channel mode this is one reaction API call per accepted message. Default off. |
@@ -369,6 +371,30 @@ platforms:
369
371
 
370
372
  Note: the per-platform `stickyMessage: <mode>` field is distinct from the top-level `Config.stickyMessage: { description, footer }` block, which still customizes the full sticky for platforms not in `hidden` mode.
371
373
 
374
+ #### End-of-turn marker
375
+
376
+ Integrations that read the channel (a voice front end, a phone bridge, a dashboard) need to know when Claude's answer is complete. The daemon streams by editing one post, so without help they can only guess from the text going quiet. With a marker, the daemon stamps the turn's last reply post the moment the turn ends:
377
+
378
+ ```yaml
379
+ platforms:
380
+ - id: slack-main
381
+ type: slack
382
+ turnMarker: metadata # reaction | metadata | off (default)
383
+ # turnMarkerEmoji: checkered_flag # reaction only; this is the default
384
+ ```
385
+
386
+ | `turnMarker` | What happens | Who sees it |
387
+ |---|---|---|
388
+ | `metadata` | the last reply post gets Slack message metadata `event_type: claude_threads_turn_complete`, `event_payload: { v, session, turn, ok }` | integrations reading `conversations.history` with `include_all_metadata=true`; invisible in the UI. Slack only |
389
+ | `reaction` | the bot reacts on the last reply post with `turnMarkerEmoji` | everyone. Slack and Mattermost |
390
+ | `off` | nothing | |
391
+
392
+ **Format stability.** The marker is a published integration contract, not an internal detail. Within a 1.x release line, `event_type` will not change, and `event_payload` will only gain fields — never lose one or change the meaning of one. `v` is the payload version; a reader should ignore fields it does not recognize and treat an unknown `v` as "marker present, payload unread". `session` is `platformId:threadId` and is stable across bot restarts and Claude respawns. `turn` counts turns within one bot process: it restarts at 1 after a bot restart and is an ordering hint within a single run, never a unique key. Readers that need exactly-once should dedupe by post id.
393
+
394
+ `ok` is false when the turn ended with an error. A turn with no reply post marks nothing. A marker failure never touches the reply.
395
+
396
+ Each marked turn costs one extra API call (a `chat.update` or `reactions.add`) beyond the streaming writes.
397
+
372
398
  ### Memory (`memory`, default: fully enabled)
373
399
 
374
400
  Each platform instance (≈ one channel) can carry persistent memory, modeled on
@@ -663,6 +689,7 @@ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions
663
689
  | `CLAUDE_PATH` | Path to the `claude` binary. Overrides the PATH lookup and the common install locations. | `claude` (from PATH) |
664
690
  | `DECISION_BRIDGE_TIMEOUT_MS` | How long the MCP permission server waits for a plan approval or question answer routed through the decision bridge (the bot's reaction UI) before falling back to the legacy behavior (generic prompt for plans, auto-allow for questions). | `3600000` (1 h) |
665
691
  | `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` | Strip `ANTHROPIC_*`, `AWS_*_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `GOOGLE_APPLICATION_CREDENTIALS`, and similar from Bash, hook, and stdio-MCP subprocesses Claude spawns. Bot-specific vars like `PLATFORM_TOKEN` pass through. **Also forces permission mode to `default`**; `--dangerously-skip-permissions` will be rejected. Requires Claude CLI 2.1.83+. | - |
692
+ | `CLAUDE_THREADS_HOME` | Root under which the bot keeps its state (`<root>/.config/claude-threads`, `<root>/.claude-threads`): config, sessions, logs, audit, memory, worktrees, update state. Lets two bots run under the same user without sharing any of it. One process per root; a second start against the same root is refused. | `$HOME` |
666
693
  | `CLAUDE_THREADS_SESSIONS_PATH` | Override the path to the persisted sessions file (default `~/.config/claude-threads/sessions.json`). | - |
667
694
  | `CLAUDE_THREADS_GITHUB_EMAILS_PATH` | Override the path to the GitHub-emails store used for commit attribution. | - |
668
695
  | `CLAUDE_THREADS_MEMORY_DIR` | Override the root of the persistent memory storage (default `~/.config/claude-threads/memory/`). | - |
@@ -37,6 +37,9 @@ ExecStart=/usr/bin/env claude-threads-daemon
37
37
  # This is a fallback for unexpected crashes
38
38
  Restart=on-failure
39
39
  RestartSec=10
40
+ # Exit 3 = another instance holds the state directory (see CLAUDE_THREADS_HOME);
41
+ # restarting would only collide again.
42
+ RestartPreventExitStatus=3
40
43
 
41
44
  # Resource limits (adjust as needed)
42
45
  # LimitNOFILE=65536