claude-threads 1.36.0 → 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.
package/dist/index.js CHANGED
@@ -2128,6 +2128,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
2128
2128
  exports.useColor = useColor;
2129
2129
  });
2130
2130
 
2131
+ // src/utils/state-home.ts
2132
+ import { homedir } from "os";
2133
+ import { resolve } from "path";
2134
+ function stateHome() {
2135
+ return resolve(process.env.CLAUDE_THREADS_HOME || homedir());
2136
+ }
2137
+ function hasStateHomeOverride() {
2138
+ return stateHome() !== resolve(homedir());
2139
+ }
2140
+ var init_state_home = () => {};
2141
+
2131
2142
  // src/utils/logger.ts
2132
2143
  function setLogHandler(handler) {
2133
2144
  globalLogHandler = handler;
@@ -2221,6 +2232,23 @@ function resolveOverheadVisibility(value, fieldPath) {
2221
2232
  return value;
2222
2233
  throw new Error(`Invalid ${fieldPath}: expected one of ${OVERHEAD_VISIBILITY_VALUES.join(", ")}, got ${JSON.stringify(value)}`);
2223
2234
  }
2235
+ function resolveTurnMarker(mode, emoji, platformType, fieldPath) {
2236
+ const m = mode === undefined || mode === null ? "off" : mode;
2237
+ if (!TURN_MARKER_VALUES.includes(m)) {
2238
+ throw new Error(`Invalid ${fieldPath}.turnMarker: expected one of ${TURN_MARKER_VALUES.join(", ")}, got ${JSON.stringify(mode)}`);
2239
+ }
2240
+ if (m === "metadata" && platformType !== "slack") {
2241
+ throw new Error(`Invalid ${fieldPath}.turnMarker: metadata is a Slack feature; use reaction on ${platformType}`);
2242
+ }
2243
+ if (emoji !== undefined && emoji !== null) {
2244
+ if (typeof emoji !== "string" || !/^[a-z0-9_+-]+$/.test(emoji)) {
2245
+ throw new Error(`Invalid ${fieldPath}.turnMarkerEmoji: expected an emoji name like checkered_flag, got ${JSON.stringify(emoji)}`);
2246
+ }
2247
+ }
2248
+ if (m === "reaction")
2249
+ return { mode: "reaction", emoji: emoji ?? DEFAULT_TURN_MARKER_EMOJI };
2250
+ return { mode: m };
2251
+ }
2224
2252
  function resolveMemoryConfig(value, fieldPath) {
2225
2253
  if (value === undefined || value === null || value === true)
2226
2254
  return DEFAULT_MEMORY_CONFIG;
@@ -2395,10 +2423,12 @@ function effectivePermissionMode(input) {
2395
2423
  return "default";
2396
2424
  return input.botWideMode;
2397
2425
  }
2398
- var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
2426
+ var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", TURN_MARKER_VALUES, DEFAULT_TURN_MARKER, DEFAULT_TURN_MARKER_EMOJI = "checkered_flag", TURN_COMPLETE_EVENT_TYPE = "claude_threads_turn_complete", TURN_COMPLETE_PAYLOAD_VERSION = 1, DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
2399
2427
  var init_types = __esm(() => {
2400
2428
  OVERHEAD_VISIBILITY_VALUES = ["full", "minimal", "hidden"];
2401
2429
  RECONNECT_POLICY_VALUES = ["retry", "exit"];
2430
+ TURN_MARKER_VALUES = ["reaction", "metadata", "off"];
2431
+ DEFAULT_TURN_MARKER = { mode: "off" };
2402
2432
  DEFAULT_MEMORY_CONFIG = {
2403
2433
  enabled: true,
2404
2434
  repoLayer: true,
@@ -4597,7 +4627,7 @@ var init_rate_limit_detector = __esm(() => {
4597
4627
 
4598
4628
  // src/claude/cli.ts
4599
4629
  import { EventEmitter } from "events";
4600
- import { resolve as resolve2, dirname as dirname2 } from "path";
4630
+ import { resolve as resolve3, dirname as dirname2 } from "path";
4601
4631
  import { fileURLToPath } from "url";
4602
4632
  import { existsSync as existsSync4, readFileSync as readFileSync2, watchFile, unwatchFile, unlinkSync, statSync, readdirSync, writeFileSync as writeFileSync2 } from "fs";
4603
4633
  import { tmpdir as tmpdir2 } from "os";
@@ -5139,15 +5169,15 @@ var init_cli = __esm(() => {
5139
5169
  getMcpServerPath() {
5140
5170
  const __filename2 = fileURLToPath(import.meta.url);
5141
5171
  const __dirname2 = dirname2(__filename2);
5142
- const bundledPath = resolve2(__dirname2, "mcp", "mcp-server.js");
5172
+ const bundledPath = resolve3(__dirname2, "mcp", "mcp-server.js");
5143
5173
  if (existsSync4(bundledPath)) {
5144
5174
  return bundledPath;
5145
5175
  }
5146
- const sourceLayoutPath = resolve2(__dirname2, "..", "mcp", "mcp-server.js");
5176
+ const sourceLayoutPath = resolve3(__dirname2, "..", "mcp", "mcp-server.js");
5147
5177
  if (existsSync4(sourceLayoutPath)) {
5148
5178
  return sourceLayoutPath;
5149
5179
  }
5150
- const tsPath = resolve2(__dirname2, "..", "mcp", "mcp-server.ts");
5180
+ const tsPath = resolve3(__dirname2, "..", "mcp", "mcp-server.ts");
5151
5181
  if (existsSync4(tsPath)) {
5152
5182
  return tsPath;
5153
5183
  }
@@ -5156,15 +5186,15 @@ var init_cli = __esm(() => {
5156
5186
  getStatusLineWriterPath() {
5157
5187
  const __filename2 = fileURLToPath(import.meta.url);
5158
5188
  const __dirname2 = dirname2(__filename2);
5159
- const bundledPath = resolve2(__dirname2, "statusline", "writer.js");
5189
+ const bundledPath = resolve3(__dirname2, "statusline", "writer.js");
5160
5190
  if (existsSync4(bundledPath)) {
5161
5191
  return bundledPath;
5162
5192
  }
5163
- const sourceLayoutPath = resolve2(__dirname2, "..", "statusline", "writer.js");
5193
+ const sourceLayoutPath = resolve3(__dirname2, "..", "statusline", "writer.js");
5164
5194
  if (existsSync4(sourceLayoutPath)) {
5165
5195
  return sourceLayoutPath;
5166
5196
  }
5167
- const tsPath = resolve2(__dirname2, "..", "statusline", "writer.ts");
5197
+ const tsPath = resolve3(__dirname2, "..", "statusline", "writer.ts");
5168
5198
  if (existsSync4(tsPath)) {
5169
5199
  return tsPath;
5170
5200
  }
@@ -5411,7 +5441,7 @@ var init_render = __esm(() => {
5411
5441
  });
5412
5442
 
5413
5443
  // src/usage/index.ts
5414
- import { homedir as homedir5 } from "os";
5444
+ import { homedir as homedir2 } from "os";
5415
5445
  import path3 from "path";
5416
5446
  function toLimits(usage) {
5417
5447
  const limits = [
@@ -5461,7 +5491,7 @@ async function collectUsage(options) {
5461
5491
  }
5462
5492
  return results;
5463
5493
  }
5464
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? path3.join(homedir5(), ".claude");
5494
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? path3.join(homedir2(), ".claude");
5465
5495
  return [await readSeat(profileNameFor(configDir), configDir)];
5466
5496
  }
5467
5497
  var USAGE_PROBE_TIMEOUT_MS = 1e4;
@@ -5525,7 +5555,6 @@ var init_emoji = __esm(() => {
5525
5555
  import { randomUUID as randomUUID2 } from "crypto";
5526
5556
  import * as path4 from "path";
5527
5557
  import * as fs from "fs/promises";
5528
- import { homedir as homedir6 } from "os";
5529
5558
  async function execGit(args, cwd) {
5530
5559
  const cmd = `git ${args.join(" ")}`;
5531
5560
  log10.debug(`Executing: ${cmd}`);
@@ -5834,10 +5863,11 @@ async function removeWorktreeMetadata(worktreePath) {
5834
5863
  var log10, WORKTREES_DIR, METADATA_STORE_PATH;
5835
5864
  var init_worktree = __esm(() => {
5836
5865
  init_spawn();
5866
+ init_state_home();
5837
5867
  init_logger();
5838
5868
  log10 = createLogger("git-wt");
5839
- WORKTREES_DIR = path4.join(homedir6(), ".claude-threads", "worktrees");
5840
- METADATA_STORE_PATH = path4.join(homedir6(), ".claude-threads", "worktree-metadata.json");
5869
+ WORKTREES_DIR = path4.join(stateHome(), ".claude-threads", "worktrees");
5870
+ METADATA_STORE_PATH = path4.join(stateHome(), ".claude-threads", "worktree-metadata.json");
5841
5871
  });
5842
5872
 
5843
5873
  // node_modules/graceful-fs/polyfills.js
@@ -47494,17 +47524,17 @@ function resolveAckReaction(configured) {
47494
47524
  }
47495
47525
 
47496
47526
  // src/persistence/audit-log.ts
47527
+ init_state_home();
47497
47528
  init_logger();
47498
47529
  import { chmodSync, closeSync, constants as fsConstants, fchmodSync, lstatSync, mkdirSync, openSync, writeSync } from "fs";
47499
47530
  import { join } from "path";
47500
- import { homedir } from "os";
47501
47531
  var log = createLogger("audit");
47502
47532
  var DETAIL_MAX = 500;
47503
47533
  var enabledPlatforms = new Set;
47504
47534
  var preparedDirs = new Set;
47505
47535
  var openFds = new Map;
47506
47536
  function auditDir() {
47507
- return process.env.CLAUDE_THREADS_AUDIT_DIR || join(homedir(), ".claude-threads", "audit");
47537
+ return process.env.CLAUDE_THREADS_AUDIT_DIR || join(stateHome(), ".claude-threads", "audit");
47508
47538
  }
47509
47539
  function configureAuditLog(platformId, enabled) {
47510
47540
  if (enabled)
@@ -47612,6 +47642,20 @@ function deriveDmPlatformConfig(parent, channelId, partnerUsernames) {
47612
47642
  };
47613
47643
  }
47614
47644
 
47645
+ // src/session/lifecycle-visibility.ts
47646
+ function shouldPostLifecycle(visibility, kind) {
47647
+ if (kind === "abnormal-exit")
47648
+ return true;
47649
+ switch (visibility) {
47650
+ case "full":
47651
+ return true;
47652
+ case "minimal":
47653
+ return kind !== "idle-warning";
47654
+ case "hidden":
47655
+ return false;
47656
+ }
47657
+ }
47658
+
47615
47659
  // src/session/lifecycle-fsm.ts
47616
47660
  init_logger();
47617
47661
  var log2 = createLogger("fsm");
@@ -47718,9 +47762,9 @@ function getSessionStatus(session) {
47718
47762
  }
47719
47763
 
47720
47764
  // src/config/index.ts
47765
+ init_state_home();
47721
47766
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "fs";
47722
- import { resolve, dirname } from "path";
47723
- import { homedir as homedir2 } from "os";
47767
+ import { resolve as resolve2, dirname } from "path";
47724
47768
 
47725
47769
  // node_modules/js-yaml/dist/js-yaml.mjs
47726
47770
  function getDefaultExportFromCjs(x) {
@@ -50889,7 +50933,7 @@ function resolvePlatformMcpPosture(platforms, globalMcpServers, managedPresent =
50889
50933
  }
50890
50934
 
50891
50935
  // src/config/index.ts
50892
- var CONFIG_PATH = resolve(homedir2(), ".config", "claude-threads", "config.yaml");
50936
+ var CONFIG_PATH = resolve2(stateHome(), ".config", "claude-threads", "config.yaml");
50893
50937
  function loadConfigWithMigration() {
50894
50938
  if (existsSync2(CONFIG_PATH)) {
50895
50939
  const content = readFileSync(CONFIG_PATH, "utf-8");
@@ -51072,7 +51116,6 @@ function writeFileAtomic(file, content) {
51072
51116
 
51073
51117
  // src/persistence/session-store.ts
51074
51118
  init_logger();
51075
- import { homedir as homedir3 } from "os";
51076
51119
  import { join as join5 } from "path";
51077
51120
 
51078
51121
  // src/sponsor.ts
@@ -51097,6 +51140,7 @@ function formatMilestoneLine(formatter, milestone) {
51097
51140
  }
51098
51141
 
51099
51142
  // src/persistence/session-store.ts
51143
+ init_state_home();
51100
51144
  var log4 = createLogger("persist");
51101
51145
  function resolveEndReason(session) {
51102
51146
  if (!session.cleanedAt)
@@ -51107,7 +51151,7 @@ function isRevivable(session) {
51107
51151
  return resolveEndReason(session) !== "stopped";
51108
51152
  }
51109
51153
  var STORE_VERSION = 2;
51110
- var DEFAULT_CONFIG_DIR = join5(homedir3(), ".config", "claude-threads");
51154
+ var DEFAULT_CONFIG_DIR = join5(stateHome(), ".config", "claude-threads");
51111
51155
  var DEFAULT_SESSIONS_FILE = join5(DEFAULT_CONFIG_DIR, "sessions.json");
51112
51156
 
51113
51157
  class SessionStore {
@@ -51384,12 +51428,12 @@ class SessionStore {
51384
51428
  }
51385
51429
 
51386
51430
  // src/persistence/thread-logger.ts
51431
+ init_state_home();
51387
51432
  init_logger();
51388
51433
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, appendFileSync, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, rmdirSync, readFileSync as readFileSync4, chmodSync as chmodSync4 } from "fs";
51389
- import { homedir as homedir4 } from "os";
51390
51434
  import { join as join6, dirname as dirname3 } from "path";
51391
51435
  var log5 = createLogger("thread-log");
51392
- var LOGS_BASE_DIR = join6(homedir4(), ".claude-threads", "logs");
51436
+ var LOGS_BASE_DIR = join6(stateHome(), ".claude-threads", "logs");
51393
51437
 
51394
51438
  class ThreadLoggerImpl {
51395
51439
  platformId;
@@ -51661,14 +51705,14 @@ function readRecentLogEntries(platformId, sessionId, maxLines = 50) {
51661
51705
 
51662
51706
  // src/version.ts
51663
51707
  import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
51664
- import { dirname as dirname4, resolve as resolve3 } from "path";
51708
+ import { dirname as dirname4, resolve as resolve4 } from "path";
51665
51709
  import { fileURLToPath as fileURLToPath2 } from "url";
51666
51710
  var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
51667
51711
  function loadPackageJson() {
51668
51712
  const candidates = [
51669
- resolve3(__dirname2, "..", "package.json"),
51670
- resolve3(__dirname2, "..", "..", "package.json"),
51671
- resolve3(process.cwd(), "package.json")
51713
+ resolve4(__dirname2, "..", "package.json"),
51714
+ resolve4(__dirname2, "..", "..", "package.json"),
51715
+ resolve4(process.cwd(), "package.json")
51672
51716
  ];
51673
51717
  for (const candidate of candidates) {
51674
51718
  if (existsSync7(candidate)) {
@@ -52136,13 +52180,13 @@ ${formatter.formatBold("Reactions:")}
52136
52180
 
52137
52181
  // src/changelog.ts
52138
52182
  import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
52139
- import { dirname as dirname5, resolve as resolve4 } from "path";
52183
+ import { dirname as dirname5, resolve as resolve5 } from "path";
52140
52184
  import { fileURLToPath as fileURLToPath3 } from "url";
52141
52185
  var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
52142
52186
  function getReleaseNotes(version) {
52143
52187
  const possiblePaths = [
52144
- resolve4(__dirname3, "..", "CHANGELOG.md"),
52145
- resolve4(__dirname3, "..", "..", "CHANGELOG.md")
52188
+ resolve5(__dirname3, "..", "CHANGELOG.md"),
52189
+ resolve5(__dirname3, "..", "..", "CHANGELOG.md")
52146
52190
  ];
52147
52191
  let changelogPath = null;
52148
52192
  for (const p of possiblePaths) {
@@ -53278,15 +53322,21 @@ async function postError(session, message, addBugReaction = true) {
53278
53322
  }
53279
53323
  return result;
53280
53324
  }
53281
- async function postInteractive(session, message, reactions) {
53282
- const post = await session.platform.createInteractivePost(message, reactions, session.threadId);
53283
- updateLastMessage(session, post);
53284
- return post;
53325
+ async function postInteractive(session, message, reactions, onPostCreated) {
53326
+ const doneInFlight = typeof session.messageManager?.markInteractivePostInFlight === "function" ? session.messageManager.markInteractivePostInFlight() : () => {};
53327
+ try {
53328
+ const post = await session.platform.createInteractivePost(message, reactions, session.threadId, (created) => {
53329
+ onPostCreated?.(created);
53330
+ doneInFlight();
53331
+ });
53332
+ updateLastMessage(session, post);
53333
+ return post;
53334
+ } finally {
53335
+ doneInFlight();
53336
+ }
53285
53337
  }
53286
53338
  async function postInteractiveAndRegister(session, message, reactions, registerPost) {
53287
- const post = await postInteractive(session, message, reactions);
53288
- registerPost(post.id, session.threadId);
53289
- return post;
53339
+ return postInteractive(session, message, reactions, (created) => registerPost(created.id, session.threadId));
53290
53340
  }
53291
53341
  async function updatePost(session, postId, message) {
53292
53342
  await withErrorHandling(() => session.platform.updatePost(postId, message), { action: "Update post", session });
@@ -53326,6 +53376,8 @@ function updateLastMessage(session, post) {
53326
53376
  init_cli();
53327
53377
 
53328
53378
  // src/operations/streaming/handler.ts
53379
+ init_state_home();
53380
+ import { createHash } from "crypto";
53329
53381
  import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
53330
53382
  import { tmpdir as tmpdir3 } from "os";
53331
53383
  import { join as join8 } from "path";
@@ -53462,7 +53514,12 @@ function safeIdSegment(id) {
53462
53514
  return id.replace(/[^A-Za-z0-9._-]/g, "_");
53463
53515
  }
53464
53516
  function getSessionUploadDir(platformId, threadId) {
53465
- return join8(tmpdir3(), UPLOAD_ROOT_DIR, `${safeIdSegment(platformId)}-${safeIdSegment(threadId)}`);
53517
+ return join8(tmpdir3(), UPLOAD_ROOT_DIR, `${instancePrefix()}${safeIdSegment(platformId)}-${safeIdSegment(threadId)}`);
53518
+ }
53519
+ function instancePrefix() {
53520
+ if (!hasStateHomeOverride())
53521
+ return "";
53522
+ return createHash("sha1").update(stateHome()).digest("hex").slice(0, 12) + "-";
53466
53523
  }
53467
53524
  async function cleanupSessionUploads(platformId, threadId) {
53468
53525
  if (!platformId || !threadId)
@@ -53670,7 +53727,7 @@ function buildRestartCliOptions(session, ctx) {
53670
53727
 
53671
53728
  // src/operations/commands/handler.ts
53672
53729
  import { randomUUID as randomUUID6 } from "crypto";
53673
- import { resolve as resolve6 } from "path";
53730
+ import { resolve as resolve7 } from "path";
53674
53731
  import { existsSync as existsSync12, statSync as statSync4 } from "fs";
53675
53732
 
53676
53733
  // node_modules/update-notifier/update-notifier.js
@@ -57680,8 +57737,8 @@ function checkGitHubCli(exec = execSync2) {
57680
57737
  }
57681
57738
  return { installed: true, authenticated: true };
57682
57739
  }
57683
- async function createGitHubIssue(title, body, workingDir) {
57684
- const ghStatus = checkGitHubCli();
57740
+ async function createGitHubIssue(title, body, workingDir, exec = execSync2) {
57741
+ const ghStatus = checkGitHubCli(exec);
57685
57742
  if (!ghStatus.installed || !ghStatus.authenticated) {
57686
57743
  throw new Error(ghStatus.error);
57687
57744
  }
@@ -57689,7 +57746,7 @@ async function createGitHubIssue(title, body, workingDir) {
57689
57746
  try {
57690
57747
  writeFileSync5(bodyFile, body, "utf-8");
57691
57748
  const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
57692
- const result = execSync2(cmd, {
57749
+ const result = exec(cmd, {
57693
57750
  cwd: workingDir,
57694
57751
  encoding: "utf-8",
57695
57752
  timeout: 30000,
@@ -59621,12 +59678,13 @@ function createAppendContentOp(sessionId, content, isToolOutput) {
59621
59678
  isToolOutput
59622
59679
  };
59623
59680
  }
59624
- function createFlushOp(sessionId, reason) {
59681
+ function createFlushOp(sessionId, reason, resultOk) {
59625
59682
  return {
59626
59683
  type: "flush",
59627
59684
  sessionId,
59628
59685
  timestamp: Date.now(),
59629
- reason
59686
+ reason,
59687
+ ...resultOk === undefined ? {} : { resultOk }
59630
59688
  };
59631
59689
  }
59632
59690
  function createTaskListOp(sessionId, action, tasks, toolUseId) {
@@ -59681,6 +59739,7 @@ function createStatusUpdateOp(sessionId, options) {
59681
59739
  };
59682
59740
  }
59683
59741
  // src/operations/transformer.ts
59742
+ init_cli();
59684
59743
  function transformEvent(event, ctx) {
59685
59744
  if (event.parent_tool_use_id) {
59686
59745
  return [];
@@ -59843,7 +59902,7 @@ function transformToolResult(event, ctx) {
59843
59902
  }
59844
59903
  function transformResult(event, ctx) {
59845
59904
  const operations = [];
59846
- operations.push(createFlushOp(ctx.sessionId, "result"));
59905
+ operations.push(createFlushOp(ctx.sessionId, "result", !isErrorResultEvent(event)));
59847
59906
  const result = event;
59848
59907
  const r = result.result;
59849
59908
  operations.push(createStatusUpdateOp(ctx.sessionId, {
@@ -59926,6 +59985,9 @@ function truncateAtWord2(text, maxLength) {
59926
59985
  }
59927
59986
  return truncated + "...";
59928
59987
  }
59988
+ // src/operations/message-manager.ts
59989
+ init_types();
59990
+
59929
59991
  // src/operations/task-tracker.ts
59930
59992
  var VALID_STATUSES = new Set(["pending", "in_progress", "completed"]);
59931
59993
  var CREATED_RESULT_RE = /Task #(\S+) created/;
@@ -61156,16 +61218,26 @@ class QuestionApprovalExecutor extends BaseExecutor {
61156
61218
 
61157
61219
  ` + ctx.formatter.formatItalic("React to respond");
61158
61220
  }
61221
+ let claimed = false;
61159
61222
  const post = await ctx.createInteractivePost(message, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], {
61160
61223
  type: "plan_approval",
61161
61224
  interactionType: "plan_approval",
61162
61225
  toolUseId: op.toolUseId
61226
+ }, (created) => {
61227
+ claimed = true;
61228
+ this.state.pendingApproval = {
61229
+ postId: created.id,
61230
+ type: op.approvalType,
61231
+ toolUseId: op.toolUseId
61232
+ };
61163
61233
  });
61164
- this.state.pendingApproval = {
61165
- postId: post.id,
61166
- type: op.approvalType,
61167
- toolUseId: op.toolUseId
61168
- };
61234
+ if (!claimed) {
61235
+ this.state.pendingApproval = {
61236
+ postId: post.id,
61237
+ type: op.approvalType,
61238
+ toolUseId: op.toolUseId
61239
+ };
61240
+ }
61169
61241
  ctx.logger.debug(`Created ${op.approvalType} approval post ${formatShortId(post.id)}`);
61170
61242
  }
61171
61243
  async postCurrentQuestion(ctx) {
@@ -61191,13 +61263,21 @@ class QuestionApprovalExecutor extends BaseExecutor {
61191
61263
  message += `
61192
61264
  `;
61193
61265
  }
61266
+ let claimed = false;
61194
61267
  const reactionOptions = NUMBER_EMOJIS.slice(0, q.options.length);
61195
61268
  const post = await ctx.createInteractivePost(message, reactionOptions, {
61196
61269
  type: "question",
61197
61270
  interactionType: "question",
61198
61271
  toolUseId: this.state.pendingQuestionSet.toolUseId
61272
+ }, (created) => {
61273
+ claimed = true;
61274
+ if (this.state.pendingQuestionSet) {
61275
+ this.state.pendingQuestionSet.currentPostId = created.id;
61276
+ }
61199
61277
  });
61200
- this.state.pendingQuestionSet.currentPostId = post.id;
61278
+ if (!claimed && this.state.pendingQuestionSet) {
61279
+ this.state.pendingQuestionSet.currentPostId = post.id;
61280
+ }
61201
61281
  }
61202
61282
  async handleQuestionAnswer(postId, optionIndex, ctx) {
61203
61283
  if (!this.state.pendingQuestionSet)
@@ -61867,6 +61947,9 @@ class MessageManager {
61867
61947
  platform;
61868
61948
  postTracker;
61869
61949
  contentBreaker;
61950
+ turnMarker;
61951
+ flushInFlight = null;
61952
+ turn = 0;
61870
61953
  session;
61871
61954
  contentExecutor;
61872
61955
  taskListExecutor;
@@ -61881,6 +61964,7 @@ class MessageManager {
61881
61964
  worktreePath;
61882
61965
  worktreeBranch;
61883
61966
  registerPost;
61967
+ beginInteractivePost;
61884
61968
  updateLastMessage;
61885
61969
  buildMessageContentCallback;
61886
61970
  startTypingCallback;
@@ -61902,11 +61986,13 @@ class MessageManager {
61902
61986
  this.worktreePath = options.worktreePath;
61903
61987
  this.worktreeBranch = options.worktreeBranch;
61904
61988
  this.registerPost = options.registerPost;
61989
+ this.beginInteractivePost = options.beginInteractivePost;
61905
61990
  this.updateLastMessage = options.updateLastMessage;
61906
61991
  this.buildMessageContentCallback = options.buildMessageContent;
61907
61992
  this.startTypingCallback = options.startTyping;
61908
61993
  this.emitSessionUpdateCallback = options.emitSessionUpdate;
61909
61994
  this.flushDelayMs = options.flushDelayMs ?? MessageManager.DEFAULT_FLUSH_DELAY_MS;
61995
+ this.turnMarker = options.turnMarker ?? { mode: "off" };
61910
61996
  this.events = createMessageManagerEvents();
61911
61997
  this.contentBreaker = new DefaultContentBreaker;
61912
61998
  this.contentExecutor = new ContentExecutor({
@@ -62048,15 +62134,58 @@ class MessageManager {
62048
62134
  }
62049
62135
  async handleFlushOp(op, ctx) {
62050
62136
  this.cancelScheduledFlush();
62051
- await this.contentExecutor.executeFlush(op, ctx);
62137
+ if (this.flushInFlight)
62138
+ await this.flushInFlight.catch(() => {
62139
+ return;
62140
+ });
62141
+ if (op.reason === "result")
62142
+ this.turn++;
62143
+ await this.runTrackedFlush(op, ctx);
62144
+ if (op.reason === "result") {
62145
+ await this.markTurnComplete(ctx, op.resultOk !== false);
62146
+ }
62147
+ }
62148
+ async markTurnComplete(ctx, ok) {
62149
+ if (this.turnMarker.mode === "off")
62150
+ return;
62151
+ const { currentPostId, currentPostContent } = this.contentExecutor.getState();
62152
+ if (!currentPostId)
62153
+ return;
62154
+ try {
62155
+ if (this.turnMarker.mode === "metadata") {
62156
+ await this.platform.updatePost(currentPostId, currentPostContent, {
62157
+ metadata: {
62158
+ event_type: TURN_COMPLETE_EVENT_TYPE,
62159
+ event_payload: { v: TURN_COMPLETE_PAYLOAD_VERSION, session: this.sessionId, turn: this.turn, ok }
62160
+ }
62161
+ });
62162
+ } else {
62163
+ await this.platform.addReaction(currentPostId, this.turnMarker.emoji ?? "checkered_flag");
62164
+ }
62165
+ } catch (err) {
62166
+ const message = err.message ?? String(err);
62167
+ if (this.turnMarker.mode === "reaction" && message.includes("already_reacted"))
62168
+ return;
62169
+ ctx.logger.warn(`turn marker (${this.turnMarker.mode}) failed on ${currentPostId}: ${message}`);
62170
+ }
62171
+ }
62172
+ async runTrackedFlush(op, ctx) {
62173
+ const running = this.contentExecutor.executeFlush(op, ctx).finally(() => {
62174
+ if (this.flushInFlight === running)
62175
+ this.flushInFlight = null;
62176
+ });
62177
+ this.flushInFlight = running;
62178
+ await running;
62052
62179
  }
62053
62180
  scheduleFlush(ctx) {
62054
62181
  if (this.flushTimer)
62055
62182
  return;
62056
- this.flushTimer = setTimeout(async () => {
62183
+ this.flushTimer = setTimeout(() => {
62057
62184
  this.flushTimer = null;
62058
62185
  const flushOp = createFlushOp(this.sessionId, "soft_threshold");
62059
- await this.contentExecutor.executeFlush(flushOp, ctx);
62186
+ this.runTrackedFlush(flushOp, ctx).catch(() => {
62187
+ return;
62188
+ });
62060
62189
  }, this.flushDelayMs);
62061
62190
  }
62062
62191
  cancelScheduledFlush() {
@@ -62067,8 +62196,12 @@ class MessageManager {
62067
62196
  }
62068
62197
  async flush() {
62069
62198
  this.cancelScheduledFlush();
62199
+ if (this.flushInFlight)
62200
+ await this.flushInFlight.catch(() => {
62201
+ return;
62202
+ });
62070
62203
  const flushOp = createFlushOp(this.sessionId, "explicit");
62071
- await this.contentExecutor.executeFlush(flushOp, this.getExecutorContext());
62204
+ await this.runTrackedFlush(flushOp, this.getExecutorContext());
62072
62205
  }
62073
62206
  getExecutorContext() {
62074
62207
  return {
@@ -62086,14 +62219,25 @@ class MessageManager {
62086
62219
  this.updateLastMessage(post);
62087
62220
  return post;
62088
62221
  },
62089
- createInteractivePost: async (content, reactions, options) => {
62090
- const post = await this.platform.createInteractivePost(content, reactions, this.threadId);
62091
- this.registerPost(post.id, options);
62092
- this.updateLastMessage(post);
62093
- return post;
62222
+ createInteractivePost: async (content, reactions, options, onPostCreated) => {
62223
+ const doneInFlight = this.beginInteractivePost?.(this.threadId);
62224
+ try {
62225
+ const post = await this.platform.createInteractivePost(content, reactions, this.threadId, (created) => {
62226
+ this.registerPost(created.id, options);
62227
+ doneInFlight?.();
62228
+ onPostCreated?.(created);
62229
+ });
62230
+ this.updateLastMessage(post);
62231
+ return post;
62232
+ } finally {
62233
+ doneInFlight?.();
62234
+ }
62094
62235
  }
62095
62236
  };
62096
62237
  }
62238
+ markInteractivePostInFlight() {
62239
+ return this.beginInteractivePost?.(this.threadId) ?? (() => {});
62240
+ }
62097
62241
  setWorktreeInfo(path, branch) {
62098
62242
  this.worktreePath = path;
62099
62243
  this.worktreeBranch = branch;
@@ -62602,12 +62746,12 @@ function formatHistoryEntry(session, formatter, getThreadLink) {
62602
62746
  const topic = getHistorySessionTopic(session, formatter);
62603
62747
  const threadLink = formatter.formatLink(topic, getThreadLink(session.threadId));
62604
62748
  const displayName = session.startedByDisplayName || session.startedBy;
62605
- const isTimedOut = isRevivable(session) && session.lifecyclePostId;
62749
+ const isTimedOut = isRevivable(session);
62606
62750
  const lastActivity = new Date(session.lastActivityAt);
62607
62751
  const time = formatRelativeTimeShort(lastActivity);
62608
62752
  const prStr = session.pullRequestUrl ? ` · ${formatPullRequestLink(session.pullRequestUrl, formatter)}` : "";
62609
62753
  const indicator = isTimedOut ? "⏸️" : "✓";
62610
- const resumeHint = isTimedOut ? ` · ${formatter.formatItalic("react \uD83D\uDD04 to resume")}` : "";
62754
+ const resumeHint = isTimedOut ? ` · ${formatter.formatItalic(session.lifecyclePostId ? "react \uD83D\uDD04 to resume" : "send a message to resume")}` : "";
62611
62755
  const lines = [];
62612
62756
  lines.push(` ${indicator} ${threadLink} · ${formatter.formatBold(displayName)}${prStr} · ${time}${resumeHint}`);
62613
62757
  if (session.sessionDescription) {
@@ -63064,19 +63208,19 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
63064
63208
  }
63065
63209
  }
63066
63210
  // src/memory/store.ts
63067
- import { createHash } from "crypto";
63211
+ init_state_home();
63212
+ import { createHash as createHash2 } from "crypto";
63068
63213
  import {
63069
63214
  existsSync as existsSync9,
63070
63215
  mkdirSync as mkdirSync5,
63071
63216
  readFileSync as readFileSync7,
63072
63217
  realpathSync
63073
63218
  } from "fs";
63074
- import { homedir as homedir7 } from "os";
63075
63219
  import { basename as basename3, dirname as dirname7, join as join10, sep as sep2 } from "path";
63076
63220
  init_logger();
63077
63221
  init_worktree();
63078
63222
  var log18 = createLogger("memory");
63079
- var DEFAULT_ROOT = join10(homedir7(), ".config", "claude-threads", "memory");
63223
+ var DEFAULT_ROOT = join10(stateHome(), ".config", "claude-threads", "memory");
63080
63224
  var CHANNEL_BLOCK_MAX_LINES = 200;
63081
63225
  var CHANNEL_BLOCK_MAX_BYTES = 25 * 1024;
63082
63226
  var CHANNEL_FILE_MAX_ENTRIES = 400;
@@ -63087,7 +63231,7 @@ function safeIdSegment2(id) {
63087
63231
  return id.replace(/[^A-Za-z0-9._-]/g, "_");
63088
63232
  }
63089
63233
  function shortHash(value, length) {
63090
- return createHash("sha256").update(value).digest("hex").slice(0, length);
63234
+ return createHash2("sha256").update(value).digest("hex").slice(0, length);
63091
63235
  }
63092
63236
  function platformSegment(platformId) {
63093
63237
  return `${safeIdSegment2(platformId) || "platform"}-${shortHash(platformId, 6)}`;
@@ -63467,10 +63611,10 @@ import { join as join12 } from "path";
63467
63611
  import { randomUUID as randomUUID3 } from "crypto";
63468
63612
 
63469
63613
  // src/persistence/platform-list-store.ts
63614
+ init_state_home();
63470
63615
  import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
63471
- import { homedir as homedir8 } from "os";
63472
63616
  import { join as join11 } from "path";
63473
- var STORES_CONFIG_DIR = join11(homedir8(), ".config", "claude-threads");
63617
+ var STORES_CONFIG_DIR = join11(stateHome(), ".config", "claude-threads");
63474
63618
  var STORE_VERSION2 = 1;
63475
63619
 
63476
63620
  class PlatformListStore {
@@ -65658,12 +65802,12 @@ async function suggestSessionMetadata(context) {
65658
65802
  init_quick_query();
65659
65803
 
65660
65804
  // src/persistence/github-emails-store.ts
65805
+ init_state_home();
65661
65806
  import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync9 } from "fs";
65662
- import { homedir as homedir9 } from "os";
65663
65807
  import { join as join14 } from "path";
65664
65808
  init_logger();
65665
65809
  var log33 = createLogger("gh-emails");
65666
- var DEFAULT_CONFIG_DIR2 = join14(homedir9(), ".config", "claude-threads");
65810
+ var DEFAULT_CONFIG_DIR2 = join14(stateHome(), ".config", "claude-threads");
65667
65811
  var DEFAULT_FILE3 = join14(DEFAULT_CONFIG_DIR2, "github-emails.yaml");
65668
65812
  var NOREPLY_REGEX = /^\d+\+[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})@users\.noreply\.github\.com$/;
65669
65813
  var STORE_VERSION3 = 1;
@@ -65923,7 +66067,7 @@ async function changeDirectory(session, newDir, username, ctx) {
65923
66067
  return;
65924
66068
  }
65925
66069
  const expandedDir = newDir.startsWith("~") ? newDir.replace("~", process.env.HOME || "") : newDir;
65926
- const absoluteDir = resolve6(expandedDir);
66070
+ const absoluteDir = resolve7(expandedDir);
65927
66071
  const formatter = session.platform.getFormatter();
65928
66072
  if (!existsSync12(absoluteDir)) {
65929
66073
  await postError(session, `Directory does not exist: ${formatter.formatCode(newDir)}`);
@@ -66414,7 +66558,7 @@ async function reportBug(session, description, username, ctx, errorContext, atta
66414
66558
  });
66415
66559
  sessionLog8(session).info(`\uD83D\uDC1B Bug report preview created by @${username}: ${title}`);
66416
66560
  }
66417
- async function handleBugReportApproval(session, isApproved, username, ctx) {
66561
+ async function handleBugReportApproval(session, isApproved, username, ctx, exec) {
66418
66562
  const pending = session.messageManager?.getPendingBugReport();
66419
66563
  if (!pending)
66420
66564
  return;
@@ -66424,7 +66568,7 @@ async function handleBugReportApproval(session, isApproved, username, ctx) {
66424
66568
  const formatter = session.platform.getFormatter();
66425
66569
  if (isApproved) {
66426
66570
  try {
66427
- const issueUrl = await createGitHubIssue(pending.title, pending.body, session.workingDir);
66571
+ const issueUrl = await createGitHubIssue(pending.title, pending.body, session.workingDir, exec);
66428
66572
  await updatePostSuccess(session, pending.postId, `${formatter.formatBold("Bug report submitted")}: ${issueUrl}`);
66429
66573
  sessionLog8(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issueUrl}`);
66430
66574
  } catch (err) {
@@ -66727,6 +66871,60 @@ class SessionRegistry {
66727
66871
  }
66728
66872
  registerPost(postId, threadId) {
66729
66873
  this.postIndex.set(postId, threadId);
66874
+ const waiters = this.pendingPostWaiters.get(postId);
66875
+ if (waiters) {
66876
+ this.pendingPostWaiters.delete(postId);
66877
+ for (const resolve of waiters)
66878
+ resolve();
66879
+ }
66880
+ }
66881
+ inFlightInteractivePosts = new Map;
66882
+ pendingPostWaiters = new Map;
66883
+ hasInFlightInteractivePost() {
66884
+ return this.inFlightInteractivePosts.size > 0;
66885
+ }
66886
+ beginInteractivePost(threadId) {
66887
+ this.inFlightInteractivePosts.set(threadId, (this.inFlightInteractivePosts.get(threadId) ?? 0) + 1);
66888
+ let done = false;
66889
+ return () => {
66890
+ if (done)
66891
+ return;
66892
+ done = true;
66893
+ const depth = (this.inFlightInteractivePosts.get(threadId) ?? 1) - 1;
66894
+ if (depth <= 0)
66895
+ this.inFlightInteractivePosts.delete(threadId);
66896
+ else
66897
+ this.inFlightInteractivePosts.set(threadId, depth);
66898
+ };
66899
+ }
66900
+ async awaitPendingPost(postId, timeoutMs) {
66901
+ if (this.postIndex.has(postId))
66902
+ return;
66903
+ if (!this.hasInFlightInteractivePost())
66904
+ return;
66905
+ await new Promise((resolve) => {
66906
+ const waiters = this.pendingPostWaiters.get(postId) ?? [];
66907
+ let settled = false;
66908
+ const finish = () => {
66909
+ if (settled)
66910
+ return;
66911
+ settled = true;
66912
+ clearTimeout(timer);
66913
+ const list = this.pendingPostWaiters.get(postId);
66914
+ if (list) {
66915
+ const idx = list.indexOf(finish);
66916
+ if (idx >= 0)
66917
+ list.splice(idx, 1);
66918
+ if (list.length === 0)
66919
+ this.pendingPostWaiters.delete(postId);
66920
+ }
66921
+ resolve();
66922
+ };
66923
+ const timer = setTimeout(finish, timeoutMs);
66924
+ timer.unref?.();
66925
+ waiters.push(finish);
66926
+ this.pendingPostWaiters.set(postId, waiters);
66927
+ });
66730
66928
  }
66731
66929
  unregisterPost(postId) {
66732
66930
  this.postIndex.delete(postId);
@@ -67244,10 +67442,12 @@ function createMessageManager(session, ctx) {
67244
67442
  sessionId: session.sessionId,
67245
67443
  worktreePath: session.worktreeInfo?.worktreePath,
67246
67444
  worktreeBranch: session.worktreeInfo?.branch,
67445
+ turnMarker: ctx.ops.getPlatformOverhead(session.platformId).turnMarker,
67247
67446
  registerPost: (postId, options) => {
67248
67447
  ctx.ops.registerPost(postId, session.threadId);
67249
67448
  postTracker.register(postId, session.threadId, session.sessionId, options);
67250
67449
  },
67450
+ beginInteractivePost: (threadId) => ctx.ops.beginInteractivePost(threadId),
67251
67451
  updateLastMessage: (post) => {
67252
67452
  updateLastMessage(session, post);
67253
67453
  },
@@ -67885,7 +68085,7 @@ ${sessionFormatter.formatItalic(outcome)}`;
67885
68085
  await withErrorHandling(() => session.platform.updatePost(postId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
67886
68086
  session.lifecyclePostId = undefined;
67887
68087
  transitionTo(session, "active");
67888
- } else {
68088
+ } else if (shouldPostLifecycle(ctx.ops.getPlatformOverhead(session.platformId).lifecycle, "resumed")) {
67889
68089
  const suffix = trigger === "platform-enabled" ? " (platform re-enabled)" : ` after bot restart (v${VERSION})`;
67890
68090
  const restartMsg = `${sessionFormatter.formatBold("Session resumed")}${suffix}
67891
68091
  ${sessionFormatter.formatItalic(outcome)}`;
@@ -68022,7 +68222,7 @@ async function handleExit(sessionId, code, ctx, source) {
68022
68222
  cleanupSessionTimers(session);
68023
68223
  await closeThreadLogger(session, "interrupt", { exitCode: code }, "pause");
68024
68224
  const message = session.lifecycle.hasClaudeResponded ? `ℹ️ Session paused. Send a new message to continue.` : `ℹ️ Session ended before Claude could respond. Send a new message to start fresh.`;
68025
- const pausePost = await withErrorHandling(() => post(session, "info", message), { action: "Post session pause notification", session });
68225
+ const pausePost = shouldPostLifecycle(ctx.ops.getPlatformOverhead(session.platformId).lifecycle, "paused") ? await withErrorHandling(() => post(session, "info", message), { action: "Post session pause notification", session }) : null;
68026
68226
  if (session.lifecycle.hasClaudeResponded) {
68027
68227
  transitionTo(session, "paused");
68028
68228
  if (pausePost) {
@@ -68101,7 +68301,7 @@ Please start a new session.`), { action: "Post session permanent failure", sessi
68101
68301
  await session.platform.unpinPost(exitTaskState.postId).catch(() => {});
68102
68302
  }
68103
68303
  await ctx.ops.flush(session);
68104
- if (code !== 0 && code !== null) {
68304
+ if (code !== 0 && code !== null && shouldPostLifecycle(ctx.ops.getPlatformOverhead(session.platformId).lifecycle, "abnormal-exit")) {
68105
68305
  const exitFormatter = session.platform.getFormatter();
68106
68306
  await post(session, "info", exitFormatter.formatBold(`[Exited: ${code}]`));
68107
68307
  }
@@ -68175,7 +68375,7 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
68175
68375
  const postId = session.lifecyclePostId;
68176
68376
  await withErrorHandling(() => session.platform.updatePost(postId, `⏱️ ${timeoutMessage}`), { action: "Update timeout post", session });
68177
68377
  } else {
68178
- const timeoutPost = await withErrorHandling(() => post(session, "timeout", timeoutMessage), { action: "Post session timeout", session });
68378
+ const timeoutPost = shouldPostLifecycle(ctx.ops.getPlatformOverhead(session.platformId).lifecycle, "timed-out") ? await withErrorHandling(() => post(session, "timeout", timeoutMessage), { action: "Post session timeout", session }) : null;
68179
68379
  if (timeoutPost) {
68180
68380
  session.lifecyclePostId = timeoutPost.id;
68181
68381
  ctx.ops.registerPost(timeoutPost.id, session.threadId);
@@ -68188,7 +68388,8 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
68188
68388
  continue;
68189
68389
  }
68190
68390
  const warningThresholdMs = timeoutMs - warningMs;
68191
- if (idleMs > warningThresholdMs && !session.timeoutWarningPosted) {
68391
+ const idleWarningWanted = shouldPostLifecycle(ctx.ops.getPlatformOverhead(session.platformId).lifecycle, "idle-warning");
68392
+ if (idleWarningWanted && idleMs > warningThresholdMs && !session.timeoutWarningPosted) {
68192
68393
  const remainingMins = Math.max(0, Math.round((timeoutMs - idleMs) / 60000));
68193
68394
  const warningFormatter = session.platform.getFormatter();
68194
68395
  const warnWaiting = session.isProcessing && (session.messageManager?.hasPendingApproval() === true || session.messageManager?.hasPendingQuestions() === true);
@@ -69846,8 +70047,9 @@ class BasePlatformClient extends EventEmitter3 {
69846
70047
  getBotName() {
69847
70048
  return this.botName;
69848
70049
  }
69849
- async createInteractivePost(message, reactions, threadId) {
70050
+ async createInteractivePost(message, reactions, threadId, onPostCreated) {
69850
70051
  const post = await this.createPost(message, threadId);
70052
+ onPostCreated?.(post);
69851
70053
  for (const emoji of reactions) {
69852
70054
  try {
69853
70055
  await this.addReaction(post.id, emoji);
@@ -70323,7 +70525,7 @@ class MattermostClient extends BasePlatformClient {
70323
70525
  const post = await this.api("POST", "/posts", request);
70324
70526
  return this.normalizePlatformPost(post);
70325
70527
  }
70326
- async updatePost(postId, message) {
70528
+ async updatePost(postId, message, _options) {
70327
70529
  const request = {
70328
70530
  id: postId,
70329
70531
  message
@@ -71371,6 +71573,9 @@ class SlackClient extends BasePlatformClient {
71371
71573
  if (resolvedThreadId) {
71372
71574
  body.thread_ts = resolvedThreadId;
71373
71575
  }
71576
+ if (options?.metadata) {
71577
+ body.metadata = options.metadata;
71578
+ }
71374
71579
  const response = await this.api("POST", "chat.postMessage", body);
71375
71580
  return {
71376
71581
  id: response.ts,
@@ -71382,13 +71587,16 @@ class SlackClient extends BasePlatformClient {
71382
71587
  createAt: Math.floor(parseFloat(response.ts) * 1000)
71383
71588
  };
71384
71589
  }
71385
- async updatePost(postId, message) {
71590
+ async updatePost(postId, message, options) {
71386
71591
  const truncatedMessage = this.truncateMessageIfNeeded(message);
71387
- const response = await this.api("POST", "chat.update", {
71592
+ const body = {
71388
71593
  channel: this.channelId,
71389
71594
  ts: postId,
71390
71595
  text: truncatedMessage
71391
- });
71596
+ };
71597
+ if (options?.metadata)
71598
+ body.metadata = options.metadata;
71599
+ const response = await this.api("POST", "chat.update", body);
71392
71600
  return {
71393
71601
  id: response.ts,
71394
71602
  platformId: this.platformId,
@@ -72662,6 +72870,7 @@ function shouldPostResumeRefusal(platformId, threadId, username, now = Date.now(
72662
72870
  // src/session/reaction-router.ts
72663
72871
  init_logger();
72664
72872
  var log52 = createLogger("manager");
72873
+ var UNKNOWN_POST_GRACE_MS = 5000;
72665
72874
  async function handleReaction(deps, platformId, postId, emojiName, username, action) {
72666
72875
  const normalizedEmoji = normalizeEmojiName(emojiName);
72667
72876
  if (action === "added" && isResumeEmoji(normalizedEmoji)) {
@@ -72669,9 +72878,13 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
72669
72878
  if (resumed)
72670
72879
  return;
72671
72880
  }
72672
- const session = deps.registry.findByPost(postId);
72673
- if (!session)
72674
- return;
72881
+ let session = deps.registry.findByPost(postId);
72882
+ if (!session) {
72883
+ await deps.registry.awaitPendingPost(postId, UNKNOWN_POST_GRACE_MS);
72884
+ session = deps.registry.findByPost(postId);
72885
+ if (!session)
72886
+ return;
72887
+ }
72675
72888
  if (session.platformId !== platformId)
72676
72889
  return;
72677
72890
  const ownerScoped = resolveApprovals(session.platform.approvals, isDcmThreadId(session.threadId)) === "owner";
@@ -72875,7 +73088,9 @@ class SessionManager extends EventEmitter4 {
72875
73088
  this.platforms.set(platformId, client);
72876
73089
  this.platformOverhead.set(platformId, {
72877
73090
  sessionHeader: options?.overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
72878
- stickyMessage: options?.overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
73091
+ stickyMessage: options?.overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY,
73092
+ lifecycle: options?.overhead?.lifecycle ?? DEFAULT_OVERHEAD_VISIBILITY,
73093
+ turnMarker: options?.overhead?.turnMarker ?? DEFAULT_TURN_MARKER
72879
73094
  });
72880
73095
  this.platformMemory.set(platformId, options?.memory ?? DEFAULT_MEMORY_CONFIG);
72881
73096
  this.platformRoutines.set(platformId, options?.routinesEnabled ?? true);
@@ -72975,6 +73190,7 @@ class SessionManager extends EventEmitter4 {
72975
73190
  getSessionId: (pid, tid) => this.getSessionId(pid, tid),
72976
73191
  findSessionByThreadId: (tid) => this.findSessionByThreadId(tid),
72977
73192
  registerPost: (pid, tid) => this.registerPost(pid, tid),
73193
+ beginInteractivePost: (tid) => this.registry.beginInteractivePost(tid),
72978
73194
  flush: async (s) => {
72979
73195
  if (s.messageManager) {
72980
73196
  await s.messageManager.flush();
@@ -73012,7 +73228,9 @@ class SessionManager extends EventEmitter4 {
73012
73228
  getClaudeAccountPoolStatus: () => this.accountPool.status(),
73013
73229
  getPlatformOverhead: (pid) => this.platformOverhead.get(pid) ?? {
73014
73230
  sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
73015
- stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
73231
+ stickyMessage: DEFAULT_OVERHEAD_VISIBILITY,
73232
+ lifecycle: DEFAULT_OVERHEAD_VISIBILITY,
73233
+ turnMarker: DEFAULT_TURN_MARKER
73016
73234
  },
73017
73235
  getPlatformMemoryConfig: (pid) => this.platformMemory.get(pid) ?? DEFAULT_MEMORY_CONFIG,
73018
73236
  isRoutinesEnabled: (pid) => this.platformRoutines.get(pid) ?? true,
@@ -73305,7 +73523,7 @@ class SessionManager extends EventEmitter4 {
73305
73523
  const pauseMessage = `⏸️ ${fmt.formatBold("Platform disabled")} - session paused. Re-enable platform to resume.`;
73306
73524
  if (session.lifecyclePostId) {
73307
73525
  await session.platform.updatePost(session.lifecyclePostId, pauseMessage);
73308
- } else {
73526
+ } else if (shouldPostLifecycle(this.platformOverhead.get(session.platformId)?.lifecycle ?? DEFAULT_OVERHEAD_VISIBILITY, "paused")) {
73309
73527
  const post = await session.platform.createPost(pauseMessage, session.threadId);
73310
73528
  session.lifecyclePostId = post.id;
73311
73529
  }
@@ -73797,7 +74015,7 @@ Mention me to start a session in this worktree.`, threadId);
73797
74015
  const shutdownMessage = `⏸️ ${fmt.formatBold("Bot shutting down")} - session will resume on restart`;
73798
74016
  if (session.lifecyclePostId) {
73799
74017
  await session.platform.updatePost(session.lifecyclePostId, shutdownMessage);
73800
- } else {
74018
+ } else if (shouldPostLifecycle(this.platformOverhead.get(session.platformId)?.lifecycle ?? DEFAULT_OVERHEAD_VISIBILITY, "shutdown")) {
73801
74019
  const post = await session.platform.createPost(shutdownMessage, session.threadId);
73802
74020
  session.lifecyclePostId = post.id;
73803
74021
  }
@@ -86393,10 +86611,11 @@ class UpdateScheduler extends EventEmitter8 {
86393
86611
  }
86394
86612
 
86395
86613
  // src/auto-update/installer.ts
86614
+ init_state_home();
86396
86615
  import { spawn as spawn4, spawnSync } from "child_process";
86397
86616
  import { existsSync as existsSync17, readFileSync as readFileSync12, mkdirSync as mkdirSync8 } from "fs";
86398
- import { dirname as dirname9, resolve as resolve7 } from "path";
86399
- import { homedir as homedir10 } from "os";
86617
+ import { dirname as dirname9, resolve as resolve8 } from "path";
86618
+ import { homedir as homedir3 } from "os";
86400
86619
  init_logger();
86401
86620
  var log56 = createLogger("installer");
86402
86621
  function detectPackageManager() {
@@ -86437,7 +86656,7 @@ function normalizePath(p) {
86437
86656
  function detectOriginalInstaller() {
86438
86657
  try {
86439
86658
  const scriptPath = normalizePath(process.argv[1] || "");
86440
- const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir10(), ".bun"));
86659
+ const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve8(homedir3(), ".bun"));
86441
86660
  if (scriptPath.startsWith(bunGlobalDir)) {
86442
86661
  return "bun";
86443
86662
  }
@@ -86457,7 +86676,7 @@ function detectOriginalInstaller() {
86457
86676
  return null;
86458
86677
  }
86459
86678
  }
86460
- var STATE_PATH = resolve7(homedir10(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
86679
+ var STATE_PATH = resolve8(stateHome(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
86461
86680
  var PACKAGE_NAME2 = "claude-threads";
86462
86681
  function loadUpdateState() {
86463
86682
  try {
@@ -86879,6 +87098,102 @@ ${getRollbackInstructions(VERSION)}`).catch(() => {});
86879
87098
  this.callbacks.refreshUI().catch(() => {});
86880
87099
  }
86881
87100
  }
87101
+ // src/utils/instance-lock.ts
87102
+ init_state_home();
87103
+ init_logger();
87104
+ import { closeSync as closeSync2, fstatSync, linkSync, mkdirSync as mkdirSync9, openSync as openSync2, readFileSync as readFileSync13, renameSync as renameSync2, statSync as statSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
87105
+ import { dirname as dirname10, join as join19 } from "path";
87106
+ var log59 = createLogger("lock");
87107
+ var LOCKED_EXIT_CODE = 3;
87108
+ function acquireInstanceLock() {
87109
+ const path = join19(stateHome(), ".config", "claude-threads", "instance.lock");
87110
+ mkdirSync9(dirname10(path), { recursive: true, mode: 448 });
87111
+ const tmp = `${path}.${process.pid}`;
87112
+ writeFileSync6(tmp, String(process.pid), { mode: 384 });
87113
+ try {
87114
+ for (let attempt = 0;attempt < 5; attempt++) {
87115
+ let linked;
87116
+ try {
87117
+ linked = tryLink(tmp, path);
87118
+ } catch (err) {
87119
+ log59.warn(`instance lock unavailable on this filesystem (${err.code ?? String(err)}); running without it`);
87120
+ return () => {};
87121
+ }
87122
+ if (linked)
87123
+ return () => release(path);
87124
+ const seen = inspect(path);
87125
+ if (!seen)
87126
+ continue;
87127
+ if (seen.pid === process.pid)
87128
+ return () => release(path);
87129
+ if (seen.pid && isAlive(seen.pid)) {
87130
+ throw new Error(`another claude-threads instance (pid ${seen.pid}) already uses ${dirname10(path)} — ` + `set CLAUDE_THREADS_HOME to run a second bot; if pid ${seen.pid} is not a claude-threads process, delete ${path}`);
87131
+ }
87132
+ const aside = `${path}.stale.${process.pid}`;
87133
+ if (!tryRename(path, aside))
87134
+ continue;
87135
+ if (statSync6(aside).ino !== seen.ino) {
87136
+ tryLink(aside, path);
87137
+ unlinkSync4(aside);
87138
+ continue;
87139
+ }
87140
+ unlinkSync4(aside);
87141
+ }
87142
+ throw new Error(`could not acquire ${path}: lost the race five times`);
87143
+ } finally {
87144
+ try {
87145
+ unlinkSync4(tmp);
87146
+ } catch {}
87147
+ }
87148
+ }
87149
+ function inspect(path) {
87150
+ let fd;
87151
+ try {
87152
+ fd = openSync2(path, "r");
87153
+ } catch {
87154
+ return;
87155
+ }
87156
+ try {
87157
+ return { pid: Number(readFileSync13(fd, "utf8").trim()) || undefined, ino: fstatSync(fd).ino };
87158
+ } finally {
87159
+ closeSync2(fd);
87160
+ }
87161
+ }
87162
+ function tryLink(src, path) {
87163
+ try {
87164
+ linkSync(src, path);
87165
+ return true;
87166
+ } catch (err) {
87167
+ if (err.code === "EEXIST")
87168
+ return false;
87169
+ throw err;
87170
+ }
87171
+ }
87172
+ function tryRename(from, to) {
87173
+ try {
87174
+ renameSync2(from, to);
87175
+ return true;
87176
+ } catch (err) {
87177
+ if (err.code === "ENOENT")
87178
+ return false;
87179
+ throw err;
87180
+ }
87181
+ }
87182
+ function release(path) {
87183
+ try {
87184
+ if (inspect(path)?.pid === process.pid)
87185
+ unlinkSync4(path);
87186
+ } catch {}
87187
+ }
87188
+ function isAlive(pid) {
87189
+ try {
87190
+ process.kill(pid, 0);
87191
+ return true;
87192
+ } catch (err) {
87193
+ return err.code === "EPERM";
87194
+ }
87195
+ }
87196
+
86882
87197
  // src/index.ts
86883
87198
  function createPlatformClient(config) {
86884
87199
  switch (config.type) {
@@ -87108,6 +87423,15 @@ async function startWithoutDaemon() {
87108
87423
  console.error(yellow(` ⚠️ --skip-version-check: ${prefix}${claudeValidation.message}`));
87109
87424
  console.error("");
87110
87425
  }
87426
+ let releaseInstanceLock;
87427
+ try {
87428
+ releaseInstanceLock = acquireInstanceLock();
87429
+ } catch (err) {
87430
+ console.error(red(` ❌ ${err instanceof Error ? err.message : String(err)}`));
87431
+ console.error("");
87432
+ process.exit(LOCKED_EXIT_CODE);
87433
+ }
87434
+ process.on("exit", () => releaseInstanceLock());
87111
87435
  if (process.env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB === "1") {
87112
87436
  const hasSkipPermissionPlatform = config.platforms.some((p) => p.skipPermissions === true);
87113
87437
  if (hasSkipPermissionPlatform) {
@@ -87286,7 +87610,9 @@ async function startWithoutDaemon() {
87286
87610
  session.addPlatform(platformConfig.id, client, {
87287
87611
  overhead: {
87288
87612
  sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
87289
- stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
87613
+ stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`),
87614
+ lifecycle: resolveOverheadVisibility(platformConfig.lifecycle, `platforms[${platformConfig.id}].lifecycle`),
87615
+ turnMarker: resolveTurnMarker(platformConfig.turnMarker, platformConfig.turnMarkerEmoji, platformConfig.type, `platforms[${platformConfig.id}]`)
87290
87616
  },
87291
87617
  memory: resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`),
87292
87618
  routinesEnabled: resolveRoutinesEnabled(platformConfig.routines, `platforms[${platformConfig.id}].routines`),
@@ -87313,7 +87639,9 @@ async function startWithoutDaemon() {
87313
87639
  session.addPlatform(dmConfig.id, dmClient, {
87314
87640
  overhead: {
87315
87641
  sessionHeader: resolveOverheadVisibility(dmConfig.sessionHeader, `dm[${dmConfig.id}].sessionHeader`),
87316
- stickyMessage: "hidden"
87642
+ stickyMessage: "hidden",
87643
+ lifecycle: resolveOverheadVisibility(dmConfig.lifecycle, `dm[${dmConfig.id}].lifecycle`),
87644
+ turnMarker: resolveTurnMarker(dmConfig.turnMarker, dmConfig.turnMarkerEmoji, dmConfig.type, `dm[${dmConfig.id}]`)
87317
87645
  },
87318
87646
  memory: resolveMemoryConfig(dmConfig.memory, `dm[${dmConfig.id}].memory`),
87319
87647
  routinesEnabled: resolveRoutinesEnabled(dmConfig.routines, `dm[${dmConfig.id}].routines`),