claude-threads 1.21.0 → 1.21.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.21.2] - 2026-08-07
9
+
10
+ ### Added
11
+ - **Decision bridge: plan approvals and question answers now flow through one surface.** On modern Claude CLIs (verified 2.1.223), `ExitPlanMode` and `AskUserQuestion` block on the MCP permission prompt — making it the authoritative gate — while the rich UI (plan post with 👍/👎, question posts with option reactions) lives in the main bot. Users saw two competing prompts, and reacting only on the bot's UI let the MCP prompt time out into a plan denial. The bot now opens a per-session local socket (a named pipe on Windows); the MCP server forwards plan/question permission requests over it and waits (default 1h, `DECISION_BRIDGE_TIMEOUT_MS`), and the user's reaction on the bot's existing UI resolves them: plans as allow/deny, question answers riding back in the permission response's `updatedInput.answers` — the CLI then tells Claude "Your questions have been answered" / "User has approved your plan" itself (both verified against the real CLI end-to-end, including through the real built `mcp-server.js`). The stdin sends (`'approved'`/answer JSON) are suppressed when a bridge request consumed the decision, and remain as the fallback for older CLIs that don't route these tools through the permission prompt. The bridge degrades gracefully everywhere: creation failure, connect failure, or timeout falls back to the previous behavior (generic prompt for plans, auto-allow for questions). The bridge is session-scoped — it survives `!cd`/`!permissions` respawns and closes with the session; pending decisions are denied on session end or fresh-CLI restart so the MCP child is never stranded. A manual real-CLI verification script ships in `tests/e2e-real-cli/decision-bridge-e2e.ts` (not run in CI — needs live credentials).
12
+ - **Decision-bridge hardening (from two adversarial review rounds over this change).** `!approve`/`!yes` resolves a pending bridge request instead of sending a stdin message that would queue behind the blocked permission call (previously it converted an approval into an hour-long hang). The bridge server aborts the bot-side pending when the requesting MCP client disconnects (timeout, cancelled tool call, dead child), so a stale pending can never swallow the user's later reaction — it falls back to stdin instead. Claude respawns deny pending decisions unconditionally (the MCP child always dies, resume or not; previously only fresh-session respawns did). The bridge socket lives in a fresh `0700` directory (other local users can't connect regardless of umask) with a short path (safe against macOS's 104-byte `sun_path` truncation), and is closed on every teardown path including start/resume failures. multiSelect questions bypass the bridge (single-select answer format is the only one verified against the real CLI) and take the legacy path. Operator knob `DECISION_BRIDGE_TIMEOUT_MS` is now actually forwarded to the MCP child.
13
+ - **The CLI is spawned with `MCP_TOOL_TIMEOUT=3600000` when a decision bridge is configured.** Delayed-decision testing against the real CLI (2.1.223) showed it abandons a pending MCP permission call after ~2 minutes — one retry, then an error — silently capping how long a plan approval or question answer could wait, regardless of the bridge's 1-hour window. With the flag set, decisions held for 150 seconds complete end-to-end (verified empirically); 1 hour matches the bridge's `DECISION_BRIDGE_TIMEOUT_MS` default, and setting `MCP_TOOL_TIMEOUT` in the bot's own env overrides it, like the other forwarded tuning flags. Should the CLI still give up (older CLI, operator override), the bridge's disconnect-abort clears the bot-side pending state so the user's late reaction falls back to the stdin flow instead of being swallowed.
14
+
15
+ ## [1.21.1] - 2026-08-07
16
+
17
+ ### Fixed
18
+ - **A respawning session can no longer be torn down by its own dying Claude process.** Two stacked races made `!cd` / `!permissions` / worktree respawns flaky (seen as the recurring `!cd should restart Claude CLI` failure on main's Integration Tests): a last event flushed by the dying process ran `resetSessionActivity`, which unconditionally flipped the session's `restarting` state back to `active` — so when the old process's exit landed, `handleExit` mistook it for the current process dying and did a full session teardown mid-restart. `resetSessionActivity` now leaves `restarting`/`cancelling` states alone, `handleExit` ignores exits from a CLI instance that is no longer the session's current one (the exit event can be delivered after the respawn already swapped in the new instance), and a successful respawn transitions to `active` explicitly instead of relying on the old exit's side effect. `ClaudeCli.kill()` gained an integration-test caller trace, mirroring the existing `sendMessage` one — attributing kills is the key question when debugging these races in CI logs.
19
+ - **`checkGitHubCli` unit tests no longer spawn the real `gh` CLI.** The two subprocess probes each carry a 5s timeout, overrunning bun's 5s per-test budget on a slow runner (the flaky `checkGitHubCli` timeout on main's CI). The exec call is now injectable and the tests cover all three outcomes (installed+authenticated, not installed, not authenticated) deterministically.
20
+ - **Integration: the `!cd` confirmation wait no longer matches the user's own message.** The loose `/changed|directory|\/tmp/i` pattern matched the `!cd /tmp` command itself, ending the test while the respawn was still in flight — `afterEach`'s `killAllSessions` then raced the restart and leaked an orphaned mock CLI into the next test.
21
+
8
22
  ## [1.21.0] - 2026-08-07
9
23
 
10
24
  ### Fixed
package/dist/index.js CHANGED
@@ -14177,7 +14177,7 @@ var require_minimist = __commonJS((exports, module) => {
14177
14177
  // node_modules/rc/index.js
14178
14178
  var require_rc = __commonJS((exports, module) => {
14179
14179
  var cc = require_utils();
14180
- var join10 = __require("path").join;
14180
+ var join11 = __require("path").join;
14181
14181
  var deepExtend = require_deep_extend();
14182
14182
  var etc = "/etc";
14183
14183
  var win = process.platform === "win32";
@@ -14203,15 +14203,15 @@ var require_rc = __commonJS((exports, module) => {
14203
14203
  }
14204
14204
  if (!win)
14205
14205
  [
14206
- join10(etc, name, "config"),
14207
- join10(etc, name + "rc")
14206
+ join11(etc, name, "config"),
14207
+ join11(etc, name + "rc")
14208
14208
  ].forEach(addConfigFile);
14209
14209
  if (home)
14210
14210
  [
14211
- join10(home, ".config", name, "config"),
14212
- join10(home, ".config", name),
14213
- join10(home, "." + name, "config"),
14214
- join10(home, "." + name + "rc")
14211
+ join11(home, ".config", name, "config"),
14212
+ join11(home, ".config", name),
14213
+ join11(home, "." + name, "config"),
14214
+ join11(home, "." + name + "rc")
14215
14215
  ].forEach(addConfigFile);
14216
14216
  addConfigFile(cc.find("." + name + "rc"));
14217
14217
  if (env3.config)
@@ -55960,7 +55960,7 @@ function cleanupBrowserBridgeSockets() {
55960
55960
  log8.debug(`Browser bridge cleanup failed: ${err}`);
55961
55961
  }
55962
55962
  }
55963
- function buildClaudeChildEnv(parentEnv, account) {
55963
+ function buildClaudeChildEnv(parentEnv, account, opts) {
55964
55964
  const env = { ...parentEnv };
55965
55965
  if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
55966
55966
  env.MCP_CONNECTION_NONBLOCKING = "true";
@@ -55968,6 +55968,9 @@ function buildClaudeChildEnv(parentEnv, account) {
55968
55968
  if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
55969
55969
  env.ENABLE_PROMPT_CACHING_1H = "true";
55970
55970
  }
55971
+ if (opts?.decisionBridge && env.MCP_TOOL_TIMEOUT === undefined) {
55972
+ env.MCP_TOOL_TIMEOUT = "3600000";
55973
+ }
55971
55974
  if (account?.home) {
55972
55975
  env.HOME = account.home;
55973
55976
  env.USERPROFILE = account.home;
@@ -56016,6 +56019,12 @@ function buildPermissionArgs(opts) {
56016
56019
  PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
56017
56020
  SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
56018
56021
  };
56022
+ if (opts.decisionBridgePath) {
56023
+ mcpEnv.DECISION_BRIDGE_PATH = opts.decisionBridgePath;
56024
+ if (process.env.DECISION_BRIDGE_TIMEOUT_MS) {
56025
+ mcpEnv.DECISION_BRIDGE_TIMEOUT_MS = process.env.DECISION_BRIDGE_TIMEOUT_MS;
56026
+ }
56027
+ }
56019
56028
  if (opts.platformConfig.appToken) {
56020
56029
  mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
56021
56030
  }
@@ -56156,7 +56165,8 @@ class ClaudeCli extends EventEmitter2 {
56156
56165
  workingDir: this.options.workingDir,
56157
56166
  uploadDir: this.options.uploadDir,
56158
56167
  outboundFiles: this.options.outboundFiles,
56159
- sessionOwnerUsername: this.options.sessionOwnerUsername
56168
+ sessionOwnerUsername: this.options.sessionOwnerUsername,
56169
+ decisionBridgePath: this.options.decisionBridgePath
56160
56170
  });
56161
56171
  args.push(...permResult.args);
56162
56172
  this.mcpConfigTempFile = permResult.tempFile;
@@ -56341,6 +56351,12 @@ class ClaudeCli extends EventEmitter2 {
56341
56351
  const pid = proc.pid;
56342
56352
  this.process = null;
56343
56353
  this.log.debug(`Killing Claude process (pid=${pid})`);
56354
+ if (process.env.INTEGRATION_TEST === "1") {
56355
+ const stack = new Error().stack?.split(`
56356
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56357
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56358
+ `);
56359
+ }
56344
56360
  return new Promise((resolve4) => {
56345
56361
  this.log.debug("Sending first SIGINT");
56346
56362
  proc.kill("SIGINT");
@@ -56384,7 +56400,9 @@ class ClaudeCli extends EventEmitter2 {
56384
56400
  return true;
56385
56401
  }
56386
56402
  buildChildEnv() {
56387
- return buildClaudeChildEnv(process.env, this.options.account);
56403
+ return buildClaudeChildEnv(process.env, this.options.account, {
56404
+ decisionBridge: this.options.decisionBridgePath !== undefined
56405
+ });
56388
56406
  }
56389
56407
  getMcpServerPath() {
56390
56408
  const __filename2 = fileURLToPath3(import.meta.url);
@@ -57237,6 +57255,109 @@ function isAuthorizedForSession(check) {
57237
57255
  return sessionAllowedUsers?.has(username) ?? false;
57238
57256
  }
57239
57257
 
57258
+ // src/mcp/decision-bridge.ts
57259
+ import { createServer, createConnection } from "node:net";
57260
+ import { tmpdir as tmpdir2 } from "node:os";
57261
+ import { join as join9 } from "node:path";
57262
+ import { randomUUID as randomUUID2 } from "node:crypto";
57263
+ import { mkdtempSync } from "node:fs";
57264
+ import { rm as rm2 } from "node:fs/promises";
57265
+
57266
+ class BridgeUnavailableError extends Error {
57267
+ }
57268
+ function bridgeSocketPath() {
57269
+ if (process.platform === "win32") {
57270
+ return `\\\\.\\pipe\\ctb-${randomUUID2()}`;
57271
+ }
57272
+ const dir = mkdtempSync(join9(tmpdir2(), "ctb-"));
57273
+ return join9(dir, "b.sock");
57274
+ }
57275
+
57276
+ class DecisionBridgeServer {
57277
+ server;
57278
+ path;
57279
+ liveSockets = new Set;
57280
+ constructor(server, path2) {
57281
+ this.server = server;
57282
+ this.path = path2;
57283
+ }
57284
+ static async create(handler) {
57285
+ const path2 = bridgeSocketPath();
57286
+ const liveSockets = new Set;
57287
+ const server = createServer((socket) => {
57288
+ liveSockets.add(socket);
57289
+ socket.once("close", () => liveSockets.delete(socket));
57290
+ let buffer = "";
57291
+ const aborter = new AbortController;
57292
+ let responded = false;
57293
+ socket.on("data", (chunk) => {
57294
+ buffer += chunk.toString("utf8");
57295
+ const newline = buffer.indexOf(`
57296
+ `);
57297
+ if (newline === -1)
57298
+ return;
57299
+ const line = buffer.slice(0, newline);
57300
+ buffer = "";
57301
+ let request;
57302
+ try {
57303
+ request = JSON.parse(line);
57304
+ } catch {
57305
+ responded = true;
57306
+ socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
57307
+ `);
57308
+ return;
57309
+ }
57310
+ handler(request, aborter.signal).then((response) => {
57311
+ responded = true;
57312
+ socket.end(JSON.stringify(response) + `
57313
+ `);
57314
+ }).catch((err) => {
57315
+ responded = true;
57316
+ if (err instanceof BridgeUnavailableError) {
57317
+ socket.destroy();
57318
+ return;
57319
+ }
57320
+ socket.end(JSON.stringify({
57321
+ behavior: "deny",
57322
+ message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
57323
+ }) + `
57324
+ `);
57325
+ });
57326
+ });
57327
+ socket.on("close", () => {
57328
+ if (!responded)
57329
+ aborter.abort();
57330
+ });
57331
+ socket.on("error", () => {});
57332
+ });
57333
+ try {
57334
+ await new Promise((resolve4, reject) => {
57335
+ server.once("error", reject);
57336
+ server.listen(path2, () => {
57337
+ server.removeListener("error", reject);
57338
+ resolve4();
57339
+ });
57340
+ });
57341
+ } catch (err) {
57342
+ if (process.platform !== "win32") {
57343
+ await rm2(join9(path2, ".."), { recursive: true, force: true }).catch(() => {});
57344
+ }
57345
+ throw err;
57346
+ }
57347
+ const bridge = new DecisionBridgeServer(server, path2);
57348
+ bridge.liveSockets = liveSockets;
57349
+ return bridge;
57350
+ }
57351
+ async close() {
57352
+ for (const socket of this.liveSockets)
57353
+ socket.destroy();
57354
+ await new Promise((resolve4) => this.server.close(() => resolve4()));
57355
+ if (process.platform !== "win32") {
57356
+ await rm2(join9(this.path, ".."), { recursive: true, force: true }).catch(() => {});
57357
+ }
57358
+ }
57359
+ }
57360
+
57240
57361
  // src/commands/registry.ts
57241
57362
  var COMMAND_REGISTRY = [
57242
57363
  {
@@ -58177,7 +58298,7 @@ ${avoidCommands.map((c) => `- \`!${c.command}\` - ${c.reason}`).join(`
58177
58298
  `.trim();
58178
58299
  }
58179
58300
  // src/session/lifecycle.ts
58180
- import { randomUUID as randomUUID4 } from "crypto";
58301
+ import { randomUUID as randomUUID5 } from "crypto";
58181
58302
  import { existsSync as existsSync12 } from "fs";
58182
58303
 
58183
58304
  // src/utils/keep-alive.ts
@@ -58526,7 +58647,10 @@ function resetSessionActivity(session) {
58526
58647
  session.lastActivityAt = new Date;
58527
58648
  session.timeoutWarningPosted = false;
58528
58649
  session.lifecyclePostId = undefined;
58529
- transitionTo(session, "active");
58650
+ const state = session.lifecycle.state;
58651
+ if (state !== "restarting" && state !== "cancelling") {
58652
+ transitionTo(session, "active");
58653
+ }
58530
58654
  if (session.worktreeInfo?.worktreePath) {
58531
58655
  updateWorktreeActivity(session.worktreeInfo.worktreePath, session.sessionId);
58532
58656
  }
@@ -58540,23 +58664,23 @@ function updateLastMessage(session, post2) {
58540
58664
 
58541
58665
  // src/operations/streaming/handler.ts
58542
58666
  init_logger();
58543
- import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
58544
- import { tmpdir as tmpdir2 } from "os";
58545
- import { join as join9 } from "path";
58667
+ import { lstat, mkdir as mkdir2, mkdtemp, rm as rm3, writeFile as writeFile2 } from "fs/promises";
58668
+ import { tmpdir as tmpdir3 } from "os";
58669
+ import { join as join10 } from "path";
58546
58670
  var log19 = createLogger("streaming");
58547
58671
  var UPLOAD_ROOT_DIR = "claude-threads-uploads";
58548
58672
  function safeIdSegment(id) {
58549
58673
  return id.replace(/[^A-Za-z0-9._-]/g, "_");
58550
58674
  }
58551
58675
  function getSessionUploadDir(platformId, threadId) {
58552
- return join9(tmpdir2(), UPLOAD_ROOT_DIR, `${safeIdSegment(platformId)}-${safeIdSegment(threadId)}`);
58676
+ return join10(tmpdir3(), UPLOAD_ROOT_DIR, `${safeIdSegment(platformId)}-${safeIdSegment(threadId)}`);
58553
58677
  }
58554
58678
  async function cleanupSessionUploads(platformId, threadId) {
58555
58679
  if (!platformId || !threadId)
58556
58680
  return;
58557
58681
  const dir = getSessionUploadDir(platformId, threadId);
58558
58682
  try {
58559
- await rm2(dir, { recursive: true, force: true });
58683
+ await rm3(dir, { recursive: true, force: true });
58560
58684
  } catch (err) {
58561
58685
  log19.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
58562
58686
  }
@@ -58582,13 +58706,13 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
58582
58706
  log19.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
58583
58707
  return { saved, skipped };
58584
58708
  }
58585
- const messageDir = await mkdtemp(join9(uploadDir, `${Date.now().toString(36)}-`));
58709
+ const messageDir = await mkdtemp(join10(uploadDir, `${Date.now().toString(36)}-`));
58586
58710
  const usedNames = new Set;
58587
58711
  for (const file of files) {
58588
58712
  try {
58589
58713
  const buffer = await platform.downloadFile(file.id);
58590
58714
  const safeName = dedupeFilename(sanitizeFilename(file.name), usedNames);
58591
- const absolutePath = join9(messageDir, safeName);
58715
+ const absolutePath = join10(messageDir, safeName);
58592
58716
  await writeFile2(absolutePath, buffer, { mode: 384, flag: "wx" });
58593
58717
  saved.push({
58594
58718
  originalName: file.name,
@@ -58671,12 +58795,13 @@ function buildRestartCliOptions(session, ctx) {
58671
58795
  account: ctx.account,
58672
58796
  uploadDir: getSessionUploadDir(session.platformId, session.threadId),
58673
58797
  outboundFiles: platformMcpConfig.outboundFiles,
58674
- sessionOwnerUsername: session.startedBy
58798
+ sessionOwnerUsername: session.startedBy,
58799
+ decisionBridgePath: session.decisionBridge?.path
58675
58800
  };
58676
58801
  }
58677
58802
 
58678
58803
  // src/operations/commands/handler.ts
58679
- import { randomUUID as randomUUID3 } from "crypto";
58804
+ import { randomUUID as randomUUID4 } from "crypto";
58680
58805
  import { resolve as resolve5 } from "path";
58681
58806
  import { existsSync as existsSync11, statSync as statSync3 } from "fs";
58682
58807
 
@@ -62094,8 +62219,8 @@ init_emoji();
62094
62219
  // src/operations/bug-report/handler.ts
62095
62220
  import { execSync as execSync2 } from "child_process";
62096
62221
  import { writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
62097
- import { tmpdir as tmpdir3 } from "os";
62098
- import { join as join10 } from "path";
62222
+ import { tmpdir as tmpdir4 } from "os";
62223
+ import { join as join11 } from "path";
62099
62224
 
62100
62225
  // node_modules/@redactpii/node/lib/index.mjs
62101
62226
  class Redactor {
@@ -62641,9 +62766,9 @@ _Reported via claude-threads bug report feature_`);
62641
62766
  function escapeShell(str2) {
62642
62767
  return str2.replace(/"/g, "\\\"");
62643
62768
  }
62644
- function checkGitHubCli() {
62769
+ function checkGitHubCli(exec2 = execSync2) {
62645
62770
  try {
62646
- execSync2("gh --version", {
62771
+ exec2("gh --version", {
62647
62772
  encoding: "utf-8",
62648
62773
  timeout: 5000,
62649
62774
  stdio: ["pipe", "pipe", "pipe"]
@@ -62656,7 +62781,7 @@ function checkGitHubCli() {
62656
62781
  };
62657
62782
  }
62658
62783
  try {
62659
- execSync2("gh auth status", {
62784
+ exec2("gh auth status", {
62660
62785
  encoding: "utf-8",
62661
62786
  timeout: 5000,
62662
62787
  stdio: ["pipe", "pipe", "pipe"]
@@ -62675,7 +62800,7 @@ async function createGitHubIssue(title, body, workingDir) {
62675
62800
  if (!ghStatus.installed || !ghStatus.authenticated) {
62676
62801
  throw new Error(ghStatus.error);
62677
62802
  }
62678
- const bodyFile = join10(tmpdir3(), `bug-body-${Date.now()}.md`);
62803
+ const bodyFile = join11(tmpdir4(), `bug-body-${Date.now()}.md`);
62679
62804
  try {
62680
62805
  writeFileSync6(bodyFile, body, "utf-8");
62681
62806
  const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
@@ -66708,6 +66833,8 @@ class MessageManager {
66708
66833
  emitSessionUpdateCallback;
66709
66834
  toolStartTimes = new Map;
66710
66835
  taskTracker = new TaskTracker;
66836
+ pendingBridgePlan = null;
66837
+ pendingBridgeQuestion = null;
66711
66838
  flushTimer = null;
66712
66839
  static DEFAULT_FLUSH_DELAY_MS = 500;
66713
66840
  flushDelayMs;
@@ -67169,9 +67296,66 @@ class MessageManager {
67169
67296
  logger.debug("Reaction not handled by any executor");
67170
67297
  return false;
67171
67298
  }
67299
+ handleBridgeRequest(request, signal) {
67300
+ if (request.kind === "plan_approval") {
67301
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: "Superseded by a newer plan" });
67302
+ return new Promise((resolve5) => {
67303
+ const pending = { resolve: resolve5, input: request.input };
67304
+ this.pendingBridgePlan = pending;
67305
+ signal?.addEventListener("abort", () => {
67306
+ if (this.pendingBridgePlan === pending)
67307
+ this.pendingBridgePlan = null;
67308
+ });
67309
+ });
67310
+ }
67311
+ if (request.kind === "question") {
67312
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: "Superseded by newer questions" });
67313
+ return new Promise((resolve5) => {
67314
+ const pending = { resolve: resolve5, input: request.input };
67315
+ this.pendingBridgeQuestion = pending;
67316
+ signal?.addEventListener("abort", () => {
67317
+ if (this.pendingBridgeQuestion === pending)
67318
+ this.pendingBridgeQuestion = null;
67319
+ });
67320
+ });
67321
+ }
67322
+ return Promise.resolve({ behavior: "deny", message: `Unknown bridge request kind` });
67323
+ }
67324
+ resolveBridgePlan(approved) {
67325
+ const pending = this.pendingBridgePlan;
67326
+ if (!pending)
67327
+ return false;
67328
+ this.pendingBridgePlan = null;
67329
+ pending.resolve(approved ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The user rejected the plan." });
67330
+ return true;
67331
+ }
67332
+ resolveBridgeQuestion(answers) {
67333
+ const pending = this.pendingBridgeQuestion;
67334
+ if (!pending)
67335
+ return false;
67336
+ this.pendingBridgeQuestion = null;
67337
+ const questions = Array.isArray(pending.input.questions) ? pending.input.questions : [];
67338
+ const record = {};
67339
+ for (const a of answers) {
67340
+ const match = questions.find((q) => q.header === a.header);
67341
+ record[match?.question ?? a.header] = a.answer;
67342
+ }
67343
+ pending.resolve({
67344
+ behavior: "allow",
67345
+ updatedInput: { ...pending.input, answers: record }
67346
+ });
67347
+ return true;
67348
+ }
67349
+ denyPendingBridgeRequests(reason) {
67350
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: reason });
67351
+ this.pendingBridgePlan = null;
67352
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: reason });
67353
+ this.pendingBridgeQuestion = null;
67354
+ }
67172
67355
  clearClaudeSessionState() {
67173
67356
  this.toolStartTimes.clear();
67174
67357
  this.taskTracker.clear();
67358
+ this.denyPendingBridgeRequests("Claude was restarted before a decision was made");
67175
67359
  }
67176
67360
  reset() {
67177
67361
  this.cancelScheduledFlush();
@@ -67187,6 +67371,7 @@ class MessageManager {
67187
67371
  this.systemExecutor.reset();
67188
67372
  }
67189
67373
  dispose() {
67374
+ this.denyPendingBridgeRequests("Session ended before a decision was made");
67190
67375
  this.cancelScheduledFlush();
67191
67376
  this.postTracker.clearSession(this.sessionId);
67192
67377
  this.events.removeAllListeners();
@@ -67967,7 +68152,7 @@ async function suggestBranchNames(workingDir, userMessage) {
67967
68152
 
67968
68153
  // src/operations/worktree/handler.ts
67969
68154
  init_worktree();
67970
- import { randomUUID as randomUUID2 } from "crypto";
68155
+ import { randomUUID as randomUUID3 } from "crypto";
67971
68156
  init_logger();
67972
68157
  var log25 = createLogger("worktree");
67973
68158
  var sessionLog2 = createSessionLog(log25);
@@ -68214,7 +68399,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
68214
68399
  transitionTo(session, "restarting");
68215
68400
  await session.claude.kill();
68216
68401
  await options.flush(session);
68217
- const newSessionId = randomUUID2();
68402
+ const newSessionId = randomUUID3();
68218
68403
  session.claudeSessionId = newSessionId;
68219
68404
  const needsTitlePrompt = !session.sessionTitle;
68220
68405
  const cliOptions = {
@@ -68233,10 +68418,14 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
68233
68418
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, existing.path, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
68234
68419
  };
68235
68420
  session.messageManager?.clearClaudeSessionState();
68236
- session.claude = new ClaudeCli(cliOptions);
68237
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
68238
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
68239
- session.claude.start();
68421
+ const newClaude = new ClaudeCli(cliOptions);
68422
+ session.claude = newClaude;
68423
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68424
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68425
+ newClaude.start();
68426
+ if (isSessionRestarting(session)) {
68427
+ transitionTo(session, "active");
68428
+ }
68240
68429
  }
68241
68430
  await options.updateSessionHeader(session);
68242
68431
  await post(session, "success", `${fmt.formatBold("Joined existing worktree")} for branch ${fmt.formatCode(branch)}
@@ -68307,7 +68496,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
68307
68496
  transitionTo(session, "restarting");
68308
68497
  await session.claude.kill();
68309
68498
  await options.flush(session);
68310
- const newSessionId = randomUUID2();
68499
+ const newSessionId = randomUUID3();
68311
68500
  session.claudeSessionId = newSessionId;
68312
68501
  const needsTitlePrompt = !session.sessionTitle;
68313
68502
  const cliOptions = {
@@ -68326,10 +68515,14 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
68326
68515
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, worktreePath, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
68327
68516
  };
68328
68517
  session.messageManager?.clearClaudeSessionState();
68329
- session.claude = new ClaudeCli(cliOptions);
68330
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
68331
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
68332
- session.claude.start();
68518
+ const newClaude = new ClaudeCli(cliOptions);
68519
+ session.claude = newClaude;
68520
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68521
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68522
+ newClaude.start();
68523
+ if (isSessionRestarting(session)) {
68524
+ transitionTo(session, "active");
68525
+ }
68333
68526
  }
68334
68527
  await options.updateSessionHeader(session);
68335
68528
  const shortWorktreePath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
@@ -69227,18 +69420,25 @@ async function restartClaudeSession(session, cliOptions, ctx, actionName) {
69227
69420
  transitionTo(session, "restarting");
69228
69421
  await session.claude.kill();
69229
69422
  await ctx.ops.flush(session);
69423
+ session.messageManager?.denyPendingBridgeRequests("Claude was restarted before a decision was made");
69230
69424
  if (!cliOptions.resume) {
69231
69425
  session.messageManager?.clearClaudeSessionState();
69232
69426
  }
69233
- session.claude = new ClaudeCli(cliOptions);
69234
- session.claude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
69235
- session.claude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code));
69236
- session.claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
69427
+ const newClaude = new ClaudeCli(cliOptions);
69428
+ session.claude = newClaude;
69429
+ newClaude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
69430
+ newClaude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code, newClaude));
69431
+ newClaude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
69237
69432
  try {
69238
- session.claude.start();
69433
+ newClaude.start();
69434
+ if (isSessionRestarting(session)) {
69435
+ transitionTo(session, "active");
69436
+ }
69239
69437
  return true;
69240
69438
  } catch (err) {
69241
- transitionTo(session, "active");
69439
+ if (isSessionRestarting(session)) {
69440
+ transitionTo(session, "active");
69441
+ }
69242
69442
  await logAndNotify(err, { action: actionName, session });
69243
69443
  return false;
69244
69444
  }
@@ -69302,12 +69502,18 @@ async function approvePendingPlan(session, username, ctx) {
69302
69502
  }
69303
69503
  const { postId } = pendingApproval;
69304
69504
  sessionLog5(session).info(`✅ Plan approved by @${username} via command`);
69505
+ const viaBridge = session.messageManager?.resolveBridgePlan(true) ?? false;
69305
69506
  const formatter = session.platform.getFormatter();
69306
69507
  const statusMessage = `${formatter.formatBold("Plan approved")} by ${formatter.formatUserMention(username)} - starting implementation...`;
69307
69508
  await updatePostSuccess(session, postId, statusMessage);
69308
69509
  session.messageManager?.clearPendingApproval();
69309
69510
  session.messageManager?.clearPendingQuestionSet();
69310
69511
  session.planApproved = true;
69512
+ if (viaBridge) {
69513
+ sessionLog5(session).info("Plan approved via decision bridge (!approve)");
69514
+ ctx.ops.startTyping(session);
69515
+ return;
69516
+ }
69311
69517
  if (session.claude.isRunning()) {
69312
69518
  session.claude.sendMessage("Plan approved! Please proceed with the implementation.");
69313
69519
  ctx.ops.startTyping(session);
@@ -69370,7 +69576,7 @@ async function changeDirectory(session, newDir, username, ctx) {
69370
69576
  sessionLog5(session).debug(`Stored work summary for context preservation`);
69371
69577
  }
69372
69578
  session.workingDir = absoluteDir;
69373
- const newSessionId = randomUUID3();
69579
+ const newSessionId = randomUUID4();
69374
69580
  session.claudeSessionId = newSessionId;
69375
69581
  const appendSystemPrompt = await buildAppendSystemPrompt(session.platform, session.platformId, absoluteDir, session.threadId, session.startedBy, session.sessionAllowedUsers, CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, { userAttribution: session.userAttribution });
69376
69582
  const cliOptions = {
@@ -69887,6 +70093,8 @@ async function cleanupSession(session, ctx, options = {}) {
69887
70093
  await closeThreadLogger(session, action, details);
69888
70094
  }
69889
70095
  session.messageManager?.dispose();
70096
+ session.decisionBridge?.close();
70097
+ session.decisionBridge = undefined;
69890
70098
  ctx.ops.emitSessionRemove(session.sessionId);
69891
70099
  mutableSessions(ctx).delete(session.sessionId);
69892
70100
  if (doCleanupPostIndex) {
@@ -69904,6 +70112,8 @@ function releaseAccountIfHeld(session, ctx) {
69904
70112
  }
69905
70113
  function removeFromRegistry(session, ctx) {
69906
70114
  session.messageManager?.dispose();
70115
+ session.decisionBridge?.close();
70116
+ session.decisionBridge = undefined;
69907
70117
  ctx.ops.emitSessionRemove(session.sessionId);
69908
70118
  mutableSessions(ctx).delete(session.sessionId);
69909
70119
  cleanupPostIndex(ctx, session.threadId);
@@ -69929,6 +70139,20 @@ function findPersistedByThreadId(persisted, threadId) {
69929
70139
  }
69930
70140
  return;
69931
70141
  }
70142
+ async function createSessionDecisionBridge(ref) {
70143
+ try {
70144
+ return await DecisionBridgeServer.create(async (request, signal) => {
70145
+ const messageManager = ref.current?.messageManager;
70146
+ if (!messageManager) {
70147
+ throw new BridgeUnavailableError("Session is not ready for decisions yet");
70148
+ }
70149
+ return messageManager.handleBridgeRequest(request, signal);
70150
+ });
70151
+ } catch (err) {
70152
+ log31.warn(`Decision bridge unavailable — falling back to legacy MCP prompts: ${err}`);
70153
+ return null;
70154
+ }
70155
+ }
69932
70156
  function createMessageManager(session, ctx) {
69933
70157
  const postTracker = new PostTracker;
69934
70158
  const messageManager = new MessageManager({
@@ -69959,10 +70183,20 @@ function createMessageManager(session, ctx) {
69959
70183
  flushDelayMs: ctx.config.flushDelayMs
69960
70184
  });
69961
70185
  messageManager.events.on("question:complete", ({ toolUseId: _toolUseId, answers }) => {
70186
+ if (messageManager.resolveBridgeQuestion(answers)) {
70187
+ sessionLog6(session).info("Question answered via decision bridge");
70188
+ ctx.ops.startTyping(session);
70189
+ return;
70190
+ }
69962
70191
  const answerJson = JSON.stringify(answers);
69963
70192
  session.claude.sendMessage(answerJson);
69964
70193
  });
69965
70194
  messageManager.events.on("approval:complete", ({ toolUseId: _toolUseId, approved }) => {
70195
+ if (messageManager.resolveBridgePlan(approved)) {
70196
+ sessionLog6(session).info(`Plan ${approved ? "approved" : "denied"} via decision bridge`);
70197
+ ctx.ops.startTyping(session);
70198
+ return;
70199
+ }
69966
70200
  const response = approved ? "approved" : "denied";
69967
70201
  session.claude.sendMessage(response);
69968
70202
  });
@@ -70228,7 +70462,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
70228
70462
  const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
70229
70463
  const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
70230
70464
  platform.sendTyping(actualThreadId);
70231
- const claudeSessionId = randomUUID4();
70465
+ const claudeSessionId = randomUUID5();
70232
70466
  let workingDir = ctx.config.workingDir;
70233
70467
  let permissionMode = ctx.config.permissionMode;
70234
70468
  let forceInteractivePermissions = false;
@@ -70282,6 +70516,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
70282
70516
  if (claudeAccount) {
70283
70517
  log31.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
70284
70518
  }
70519
+ const bridgeSessionRef = {};
70520
+ const decisionBridge = await createSessionDecisionBridge(bridgeSessionRef);
70285
70521
  const cliOptions = {
70286
70522
  workingDir,
70287
70523
  threadId: actualThreadId,
@@ -70296,9 +70532,16 @@ async function startSession(options, username, displayName, replyToPostId, platf
70296
70532
  account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
70297
70533
  uploadDir: getSessionUploadDir(platformId, actualThreadId),
70298
70534
  outboundFiles: platformMcpConfig.outboundFiles,
70299
- sessionOwnerUsername: username
70535
+ sessionOwnerUsername: username,
70536
+ decisionBridgePath: decisionBridge?.path
70300
70537
  };
70301
- const claude = new ClaudeCli(cliOptions);
70538
+ let claude;
70539
+ try {
70540
+ claude = new ClaudeCli(cliOptions);
70541
+ } catch (err) {
70542
+ decisionBridge?.close();
70543
+ throw err;
70544
+ }
70302
70545
  const session = {
70303
70546
  platformId,
70304
70547
  threadId: actualThreadId,
@@ -70332,7 +70575,9 @@ async function startSession(options, username, displayName, replyToPostId, platf
70332
70575
  enabled: ctx.config.threadLogsEnabled ?? true
70333
70576
  })
70334
70577
  };
70578
+ session.decisionBridge = decisionBridge ?? undefined;
70335
70579
  session.messageManager = createMessageManager(session, ctx);
70580
+ bridgeSessionRef.current = session;
70336
70581
  session.threadLogger?.logLifecycle("start", {
70337
70582
  username,
70338
70583
  workingDir: ctx.config.workingDir
@@ -70351,7 +70596,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
70351
70596
  await ctx.ops.updateStickyMessage();
70352
70597
  ctx.ops.startTyping(session);
70353
70598
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70354
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70599
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70355
70600
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70356
70601
  try {
70357
70602
  claude.start();
@@ -70359,6 +70604,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
70359
70604
  await logAndNotify(err, { action: "Start Claude", session });
70360
70605
  ctx.ops.stopTyping(session);
70361
70606
  session.messageManager?.dispose();
70607
+ session.decisionBridge?.close();
70608
+ session.decisionBridge = undefined;
70362
70609
  ctx.ops.emitSessionRemove(session.sessionId);
70363
70610
  mutableSessions(ctx).delete(session.sessionId);
70364
70611
  releaseAccountIfHeld(session, ctx);
@@ -70442,6 +70689,8 @@ Please start a new session.`), { action: "Post resume failure notification" });
70442
70689
  if (state.claudeAccountId && !claudeAccount) {
70443
70690
  log31.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
70444
70691
  }
70692
+ const resumeBridgeRef = {};
70693
+ const resumeBridge = await createSessionDecisionBridge(resumeBridgeRef);
70445
70694
  const cliOptions = {
70446
70695
  workingDir: state.workingDir,
70447
70696
  threadId: state.threadId,
@@ -70456,9 +70705,16 @@ Please start a new session.`), { action: "Post resume failure notification" });
70456
70705
  account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
70457
70706
  uploadDir: getSessionUploadDir(platformId, state.threadId),
70458
70707
  outboundFiles: platformMcpConfig.outboundFiles,
70459
- sessionOwnerUsername: state.startedBy
70708
+ sessionOwnerUsername: state.startedBy,
70709
+ decisionBridgePath: resumeBridge?.path
70460
70710
  };
70461
- const claude = new ClaudeCli(cliOptions);
70711
+ let claude;
70712
+ try {
70713
+ claude = new ClaudeCli(cliOptions);
70714
+ } catch (err) {
70715
+ resumeBridge?.close();
70716
+ throw err;
70717
+ }
70462
70718
  const session = {
70463
70719
  platformId,
70464
70720
  threadId: state.threadId,
@@ -70504,6 +70760,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70504
70760
  enabled: ctx.config.threadLogsEnabled ?? true
70505
70761
  })
70506
70762
  };
70763
+ session.decisionBridge = resumeBridge ?? undefined;
70507
70764
  if (!session.worktreeInfo) {
70508
70765
  const detected = await detectWorktreeInfo(session.workingDir);
70509
70766
  if (detected) {
@@ -70516,6 +70773,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70516
70773
  }
70517
70774
  }
70518
70775
  session.messageManager = createMessageManager(session, ctx);
70776
+ resumeBridgeRef.current = session;
70519
70777
  await session.messageManager.restoreTaskListFromPersistence({
70520
70778
  tasksPostId: state.tasksPostId,
70521
70779
  lastTasksContent: state.lastTasksContent,
@@ -70546,7 +70804,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70546
70804
  ctx.ops.emitSessionAdd(session);
70547
70805
  keepAlive.sessionStarted();
70548
70806
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70549
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70807
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70550
70808
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70551
70809
  try {
70552
70810
  claude.start();
@@ -70571,6 +70829,8 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
70571
70829
  } catch (err) {
70572
70830
  log31.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
70573
70831
  session.messageManager?.dispose();
70832
+ session.decisionBridge?.close();
70833
+ session.decisionBridge = undefined;
70574
70834
  ctx.ops.emitSessionRemove(sessionId);
70575
70835
  mutableSessions(ctx).delete(sessionId);
70576
70836
  ctx.state.sessionStore.remove(sessionId);
@@ -70634,7 +70894,7 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
70634
70894
  log31.warn(`Failed to resume session ${shortId}..., could not send message`);
70635
70895
  }
70636
70896
  }
70637
- async function handleExit(sessionId, code, ctx) {
70897
+ async function handleExit(sessionId, code, ctx, source) {
70638
70898
  const session = mutableSessions(ctx).get(sessionId);
70639
70899
  const shortId = sessionId.substring(0, 8);
70640
70900
  sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
@@ -70642,6 +70902,10 @@ async function handleExit(sessionId, code, ctx) {
70642
70902
  log31.debug(`Session ${shortId}... not found (already cleaned up)`);
70643
70903
  return;
70644
70904
  }
70905
+ if (source && session.claude !== source) {
70906
+ sessionLog6(session).debug(`Ignoring exit from replaced Claude process`);
70907
+ return;
70908
+ }
70645
70909
  if (isSessionRestarting(session)) {
70646
70910
  sessionLog6(session).debug(`Restarting, skipping cleanup`);
70647
70911
  transitionTo(session, "active");
@@ -71365,7 +71629,7 @@ class SessionManager extends EventEmitter4 {
71365
71629
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71366
71630
  updateStickyMessage: () => this.updateStickyMessage(),
71367
71631
  handleEvent: (sid, e) => this.handleEvent(sid, e),
71368
- handleExit: (sid, code) => this.handleExit(sid, code),
71632
+ handleExit: (sid, code, source) => this.handleExit(sid, code, source),
71369
71633
  killSession: (tid) => this.killSession(tid),
71370
71634
  shouldPromptForWorktree: (s) => this.shouldPromptForWorktree(s),
71371
71635
  postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
@@ -71469,8 +71733,8 @@ class SessionManager extends EventEmitter4 {
71469
71733
  session.messageManager.handleEvent(event);
71470
71734
  handleEventPostProcessing(session, event, this.getContext());
71471
71735
  }
71472
- async handleExit(sessionId, code) {
71473
- await handleExit(sessionId, code, this.getContext());
71736
+ async handleExit(sessionId, code, source) {
71737
+ await handleExit(sessionId, code, this.getContext(), source);
71474
71738
  }
71475
71739
  startTyping(session) {
71476
71740
  const wasTyping = session.timers.typingTimer !== null;
@@ -71985,7 +72249,7 @@ class SessionManager extends EventEmitter4 {
71985
72249
  worktreeMode: this.worktreeMode,
71986
72250
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
71987
72251
  handleEvent: (tid, e) => this.handleEvent(tid, e),
71988
- handleExit: (tid, code) => this.handleExit(tid, code),
72252
+ handleExit: (tid, code, source) => this.handleExit(tid, code, source),
71989
72253
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71990
72254
  flush: async (s) => {
71991
72255
  if (s.messageManager) {
@@ -82561,7 +82825,7 @@ class UpdateInstaller {
82561
82825
  init_logger();
82562
82826
  import { spawn as spawn5 } from "child_process";
82563
82827
  import { existsSync as existsSync15, statSync as statSync4 } from "fs";
82564
- import { delimiter, join as join11 } from "path";
82828
+ import { delimiter, join as join12 } from "path";
82565
82829
  var log39 = createLogger("respawn");
82566
82830
  function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
82567
82831
  if (env5.CLAUDE_THREADS_BIN) {
@@ -82587,16 +82851,16 @@ function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync15,
82587
82851
  const path10 = _env.PATH || _env.Path || "";
82588
82852
  const dirs = path10.split(delimiter).filter(Boolean);
82589
82853
  const home = _env.HOME || _env.USERPROFILE;
82590
- const bunRoot = _env.BUN_INSTALL || (home ? join11(home, ".bun") : null);
82854
+ const bunRoot = _env.BUN_INSTALL || (home ? join12(home, ".bun") : null);
82591
82855
  if (bunRoot) {
82592
- const bunBin = join11(bunRoot, "bin");
82856
+ const bunBin = join12(bunRoot, "bin");
82593
82857
  if (!dirs.includes(bunBin)) {
82594
82858
  dirs.push(bunBin);
82595
82859
  }
82596
82860
  }
82597
82861
  for (const dir of dirs) {
82598
82862
  for (const name of names) {
82599
- const candidate = join11(dir, name);
82863
+ const candidate = join12(dir, name);
82600
82864
  if (_existsSync(candidate) && _isFileExecutable(candidate)) {
82601
82865
  return candidate;
82602
82866
  }
@@ -51569,6 +51569,8 @@ class MessageManager {
51569
51569
  emitSessionUpdateCallback;
51570
51570
  toolStartTimes = new Map;
51571
51571
  taskTracker = new TaskTracker;
51572
+ pendingBridgePlan = null;
51573
+ pendingBridgeQuestion = null;
51572
51574
  flushTimer = null;
51573
51575
  static DEFAULT_FLUSH_DELAY_MS = 500;
51574
51576
  flushDelayMs;
@@ -52030,9 +52032,66 @@ class MessageManager {
52030
52032
  logger.debug("Reaction not handled by any executor");
52031
52033
  return false;
52032
52034
  }
52035
+ handleBridgeRequest(request, signal) {
52036
+ if (request.kind === "plan_approval") {
52037
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: "Superseded by a newer plan" });
52038
+ return new Promise((resolve2) => {
52039
+ const pending = { resolve: resolve2, input: request.input };
52040
+ this.pendingBridgePlan = pending;
52041
+ signal?.addEventListener("abort", () => {
52042
+ if (this.pendingBridgePlan === pending)
52043
+ this.pendingBridgePlan = null;
52044
+ });
52045
+ });
52046
+ }
52047
+ if (request.kind === "question") {
52048
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: "Superseded by newer questions" });
52049
+ return new Promise((resolve2) => {
52050
+ const pending = { resolve: resolve2, input: request.input };
52051
+ this.pendingBridgeQuestion = pending;
52052
+ signal?.addEventListener("abort", () => {
52053
+ if (this.pendingBridgeQuestion === pending)
52054
+ this.pendingBridgeQuestion = null;
52055
+ });
52056
+ });
52057
+ }
52058
+ return Promise.resolve({ behavior: "deny", message: `Unknown bridge request kind` });
52059
+ }
52060
+ resolveBridgePlan(approved) {
52061
+ const pending = this.pendingBridgePlan;
52062
+ if (!pending)
52063
+ return false;
52064
+ this.pendingBridgePlan = null;
52065
+ pending.resolve(approved ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The user rejected the plan." });
52066
+ return true;
52067
+ }
52068
+ resolveBridgeQuestion(answers) {
52069
+ const pending = this.pendingBridgeQuestion;
52070
+ if (!pending)
52071
+ return false;
52072
+ this.pendingBridgeQuestion = null;
52073
+ const questions = Array.isArray(pending.input.questions) ? pending.input.questions : [];
52074
+ const record4 = {};
52075
+ for (const a of answers) {
52076
+ const match = questions.find((q) => q.header === a.header);
52077
+ record4[match?.question ?? a.header] = a.answer;
52078
+ }
52079
+ pending.resolve({
52080
+ behavior: "allow",
52081
+ updatedInput: { ...pending.input, answers: record4 }
52082
+ });
52083
+ return true;
52084
+ }
52085
+ denyPendingBridgeRequests(reason) {
52086
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: reason });
52087
+ this.pendingBridgePlan = null;
52088
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: reason });
52089
+ this.pendingBridgeQuestion = null;
52090
+ }
52033
52091
  clearClaudeSessionState() {
52034
52092
  this.toolStartTimes.clear();
52035
52093
  this.taskTracker.clear();
52094
+ this.denyPendingBridgeRequests("Claude was restarted before a decision was made");
52036
52095
  }
52037
52096
  reset() {
52038
52097
  this.cancelScheduledFlush();
@@ -52048,6 +52107,7 @@ class MessageManager {
52048
52107
  this.systemExecutor.reset();
52049
52108
  }
52050
52109
  dispose() {
52110
+ this.denyPendingBridgeRequests("Session ended before a decision was made");
52051
52111
  this.cancelScheduledFlush();
52052
52112
  this.postTracker.clearSession(this.sessionId);
52053
52113
  this.events.removeAllListeners();
@@ -55751,6 +55811,148 @@ class UsernameAnonymizer {
55751
55811
  return anonymized;
55752
55812
  }
55753
55813
  }
55814
+ // src/mcp/decision-bridge.ts
55815
+ import { createServer, createConnection } from "node:net";
55816
+ import { tmpdir } from "node:os";
55817
+ import { join as join3 } from "node:path";
55818
+ import { randomUUID } from "node:crypto";
55819
+ import { mkdtempSync } from "node:fs";
55820
+ import { rm } from "node:fs/promises";
55821
+
55822
+ class BridgeUnavailableError extends Error {
55823
+ }
55824
+ function bridgeSocketPath() {
55825
+ if (process.platform === "win32") {
55826
+ return `\\\\.\\pipe\\ctb-${randomUUID()}`;
55827
+ }
55828
+ const dir = mkdtempSync(join3(tmpdir(), "ctb-"));
55829
+ return join3(dir, "b.sock");
55830
+ }
55831
+
55832
+ class DecisionBridgeServer {
55833
+ server;
55834
+ path;
55835
+ liveSockets = new Set;
55836
+ constructor(server, path) {
55837
+ this.server = server;
55838
+ this.path = path;
55839
+ }
55840
+ static async create(handler2) {
55841
+ const path = bridgeSocketPath();
55842
+ const liveSockets = new Set;
55843
+ const server = createServer((socket) => {
55844
+ liveSockets.add(socket);
55845
+ socket.once("close", () => liveSockets.delete(socket));
55846
+ let buffer = "";
55847
+ const aborter = new AbortController;
55848
+ let responded = false;
55849
+ socket.on("data", (chunk) => {
55850
+ buffer += chunk.toString("utf8");
55851
+ const newline = buffer.indexOf(`
55852
+ `);
55853
+ if (newline === -1)
55854
+ return;
55855
+ const line = buffer.slice(0, newline);
55856
+ buffer = "";
55857
+ let request;
55858
+ try {
55859
+ request = JSON.parse(line);
55860
+ } catch {
55861
+ responded = true;
55862
+ socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
55863
+ `);
55864
+ return;
55865
+ }
55866
+ handler2(request, aborter.signal).then((response) => {
55867
+ responded = true;
55868
+ socket.end(JSON.stringify(response) + `
55869
+ `);
55870
+ }).catch((err) => {
55871
+ responded = true;
55872
+ if (err instanceof BridgeUnavailableError) {
55873
+ socket.destroy();
55874
+ return;
55875
+ }
55876
+ socket.end(JSON.stringify({
55877
+ behavior: "deny",
55878
+ message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
55879
+ }) + `
55880
+ `);
55881
+ });
55882
+ });
55883
+ socket.on("close", () => {
55884
+ if (!responded)
55885
+ aborter.abort();
55886
+ });
55887
+ socket.on("error", () => {});
55888
+ });
55889
+ try {
55890
+ await new Promise((resolve4, reject) => {
55891
+ server.once("error", reject);
55892
+ server.listen(path, () => {
55893
+ server.removeListener("error", reject);
55894
+ resolve4();
55895
+ });
55896
+ });
55897
+ } catch (err) {
55898
+ if (process.platform !== "win32") {
55899
+ await rm(join3(path, ".."), { recursive: true, force: true }).catch(() => {});
55900
+ }
55901
+ throw err;
55902
+ }
55903
+ const bridge = new DecisionBridgeServer(server, path);
55904
+ bridge.liveSockets = liveSockets;
55905
+ return bridge;
55906
+ }
55907
+ async close() {
55908
+ for (const socket of this.liveSockets)
55909
+ socket.destroy();
55910
+ await new Promise((resolve4) => this.server.close(() => resolve4()));
55911
+ if (process.platform !== "win32") {
55912
+ await rm(join3(this.path, ".."), { recursive: true, force: true }).catch(() => {});
55913
+ }
55914
+ }
55915
+ }
55916
+ function requestBridgeDecision(path, request, timeoutMs) {
55917
+ return new Promise((resolve4, reject) => {
55918
+ const socket = createConnection(path);
55919
+ let buffer = "";
55920
+ let settled = false;
55921
+ const fail = (err) => {
55922
+ if (settled)
55923
+ return;
55924
+ settled = true;
55925
+ clearTimeout(timer);
55926
+ socket.destroy();
55927
+ reject(err);
55928
+ };
55929
+ const timer = setTimeout(() => fail(new Error(`Bridge decision timed out after ${timeoutMs}ms`)), timeoutMs);
55930
+ socket.on("connect", () => {
55931
+ socket.write(JSON.stringify(request) + `
55932
+ `);
55933
+ });
55934
+ socket.on("data", (chunk) => {
55935
+ buffer += chunk.toString("utf8");
55936
+ const newline = buffer.indexOf(`
55937
+ `);
55938
+ if (newline === -1)
55939
+ return;
55940
+ if (settled)
55941
+ return;
55942
+ settled = true;
55943
+ clearTimeout(timer);
55944
+ socket.destroy();
55945
+ try {
55946
+ resolve4(JSON.parse(buffer.slice(0, newline)));
55947
+ } catch {
55948
+ reject(new Error("Malformed bridge response"));
55949
+ }
55950
+ });
55951
+ socket.on("error", (err) => fail(err));
55952
+ socket.on("close", () => fail(new Error("Bridge connection closed before a decision arrived")));
55953
+ });
55954
+ }
55955
+
55754
55956
  // src/utils/spawn.ts
55755
55957
  import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
55756
55958
  var isWindows = process.platform === "win32";
@@ -55769,8 +55971,8 @@ import { EventEmitter as EventEmitter2 } from "events";
55769
55971
  import { resolve as resolve4, dirname as dirname5 } from "path";
55770
55972
  import { fileURLToPath as fileURLToPath3 } from "url";
55771
55973
  import { existsSync as existsSync4, readFileSync as readFileSync3, watchFile, unwatchFile, unlinkSync, statSync, readdirSync, writeFileSync } from "fs";
55772
- import { tmpdir } from "os";
55773
- import { join as join3 } from "path";
55974
+ import { tmpdir as tmpdir2 } from "os";
55975
+ import { join as join4 } from "path";
55774
55976
 
55775
55977
  // src/mcp/outbound-env.ts
55776
55978
  var OUTBOUND_ENV = {
@@ -55864,11 +56066,11 @@ function parseRateLimitEvent(event, now = Date.now()) {
55864
56066
  var log8 = createLogger("claude");
55865
56067
  function cleanupBrowserBridgeSockets() {
55866
56068
  try {
55867
- const tempDir = tmpdir();
56069
+ const tempDir = tmpdir2();
55868
56070
  const files = readdirSync(tempDir);
55869
56071
  for (const file2 of files) {
55870
56072
  if (file2.startsWith("claude-mcp-browser-bridge-")) {
55871
- const filePath = join3(tempDir, file2);
56073
+ const filePath = join4(tempDir, file2);
55872
56074
  try {
55873
56075
  const stats = statSync(filePath);
55874
56076
  if (stats.isSocket()) {
@@ -55882,7 +56084,7 @@ function cleanupBrowserBridgeSockets() {
55882
56084
  log8.debug(`Browser bridge cleanup failed: ${err}`);
55883
56085
  }
55884
56086
  }
55885
- function buildClaudeChildEnv(parentEnv, account) {
56087
+ function buildClaudeChildEnv(parentEnv, account, opts) {
55886
56088
  const env = { ...parentEnv };
55887
56089
  if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
55888
56090
  env.MCP_CONNECTION_NONBLOCKING = "true";
@@ -55890,6 +56092,9 @@ function buildClaudeChildEnv(parentEnv, account) {
55890
56092
  if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
55891
56093
  env.ENABLE_PROMPT_CACHING_1H = "true";
55892
56094
  }
56095
+ if (opts?.decisionBridge && env.MCP_TOOL_TIMEOUT === undefined) {
56096
+ env.MCP_TOOL_TIMEOUT = "3600000";
56097
+ }
55893
56098
  if (account?.home) {
55894
56099
  env.HOME = account.home;
55895
56100
  env.USERPROFILE = account.home;
@@ -55913,8 +56118,8 @@ function materializeMcpConfig(config3, sessionId, opts = {}) {
55913
56118
  if (opts.inline) {
55914
56119
  return { mode: "inline", value: JSON.stringify(config3) };
55915
56120
  }
55916
- const dir = opts.tmpDirOverride ?? tmpdir();
55917
- const path = join3(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
56121
+ const dir = opts.tmpDirOverride ?? tmpdir2();
56122
+ const path = join4(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
55918
56123
  writeFileSync(path, JSON.stringify(config3), { mode: 384 });
55919
56124
  return { mode: "file", path };
55920
56125
  }
@@ -55938,6 +56143,12 @@ function buildPermissionArgs(opts) {
55938
56143
  PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
55939
56144
  SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
55940
56145
  };
56146
+ if (opts.decisionBridgePath) {
56147
+ mcpEnv.DECISION_BRIDGE_PATH = opts.decisionBridgePath;
56148
+ if (process.env.DECISION_BRIDGE_TIMEOUT_MS) {
56149
+ mcpEnv.DECISION_BRIDGE_TIMEOUT_MS = process.env.DECISION_BRIDGE_TIMEOUT_MS;
56150
+ }
56151
+ }
55941
56152
  if (opts.platformConfig.appToken) {
55942
56153
  mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
55943
56154
  }
@@ -56078,7 +56289,8 @@ class ClaudeCli extends EventEmitter2 {
56078
56289
  workingDir: this.options.workingDir,
56079
56290
  uploadDir: this.options.uploadDir,
56080
56291
  outboundFiles: this.options.outboundFiles,
56081
- sessionOwnerUsername: this.options.sessionOwnerUsername
56292
+ sessionOwnerUsername: this.options.sessionOwnerUsername,
56293
+ decisionBridgePath: this.options.decisionBridgePath
56082
56294
  });
56083
56295
  args.push(...permResult.args);
56084
56296
  this.mcpConfigTempFile = permResult.tempFile;
@@ -56089,7 +56301,7 @@ class ClaudeCli extends EventEmitter2 {
56089
56301
  args.push("--append-system-prompt", this.options.appendSystemPrompt);
56090
56302
  }
56091
56303
  if (this.options.sessionId) {
56092
- this.statusFilePath = join3(tmpdir(), `claude-threads-status-${this.options.sessionId}.json`);
56304
+ this.statusFilePath = join4(tmpdir2(), `claude-threads-status-${this.options.sessionId}.json`);
56093
56305
  const statusLineWriterPath = this.getStatusLineWriterPath();
56094
56306
  const statusLineSettings = {
56095
56307
  statusLine: {
@@ -56263,6 +56475,12 @@ class ClaudeCli extends EventEmitter2 {
56263
56475
  const pid = proc.pid;
56264
56476
  this.process = null;
56265
56477
  this.log.debug(`Killing Claude process (pid=${pid})`);
56478
+ if (process.env.INTEGRATION_TEST === "1") {
56479
+ const stack = new Error().stack?.split(`
56480
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56481
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56482
+ `);
56483
+ }
56266
56484
  return new Promise((resolve5) => {
56267
56485
  this.log.debug("Sending first SIGINT");
56268
56486
  proc.kill("SIGINT");
@@ -56306,7 +56524,9 @@ class ClaudeCli extends EventEmitter2 {
56306
56524
  return true;
56307
56525
  }
56308
56526
  buildChildEnv() {
56309
- return buildClaudeChildEnv(process.env, this.options.account);
56527
+ return buildClaudeChildEnv(process.env, this.options.account, {
56528
+ decisionBridge: this.options.decisionBridgePath !== undefined
56529
+ });
56310
56530
  }
56311
56531
  getMcpServerPath() {
56312
56532
  const __filename2 = fileURLToPath3(import.meta.url);
@@ -56999,10 +57219,10 @@ init_emoji();
56999
57219
 
57000
57220
  // src/persistence/github-emails-store.ts
57001
57221
  import { homedir as homedir4 } from "os";
57002
- import { join as join5 } from "path";
57222
+ import { join as join6 } from "path";
57003
57223
  var log18 = createLogger("gh-emails");
57004
- var DEFAULT_CONFIG_DIR = join5(homedir4(), ".config", "claude-threads");
57005
- var DEFAULT_FILE = join5(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57224
+ var DEFAULT_CONFIG_DIR = join6(homedir4(), ".config", "claude-threads");
57225
+ var DEFAULT_FILE = join6(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57006
57226
 
57007
57227
  // src/operations/commands/handler.ts
57008
57228
  var log19 = createLogger("commands");
@@ -58374,6 +58594,8 @@ var PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || "";
58374
58594
  var PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || "";
58375
58595
  var ALLOWED_USERS = (process.env.ALLOWED_USERS || "").split(",").map((u) => u.trim()).filter((u) => u.length > 0);
58376
58596
  var PERMISSION_TIMEOUT_MS = parseInt(process.env.PERMISSION_TIMEOUT_MS || "120000", 10);
58597
+ var DECISION_BRIDGE_PATH = process.env.DECISION_BRIDGE_PATH || "";
58598
+ var DECISION_BRIDGE_TIMEOUT_MS = parseInt(process.env.DECISION_BRIDGE_TIMEOUT_MS || "3600000", 10);
58377
58599
  var SESSION_OWNER_USERNAME = process.env.SESSION_OWNER_USERNAME || "";
58378
58600
  var SESSION_WORKING_DIR = process.env[OUTBOUND_ENV.SESSION_WORKING_DIR] || "";
58379
58601
  var SESSION_UPLOAD_DIR = process.env[OUTBOUND_ENV.SESSION_UPLOAD_DIR] || "";
@@ -58436,6 +58658,21 @@ async function handlePermissionWith(toolName, toolInput, cfg) {
58436
58658
  mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
58437
58659
  return { behavior: "allow", updatedInput: toolInput };
58438
58660
  }
58661
+ const hasMultiSelect = toolName === "AskUserQuestion" && Array.isArray(toolInput.questions) && toolInput.questions.some((q) => q?.multiSelect === true);
58662
+ if ((toolName === "ExitPlanMode" || toolName === "AskUserQuestion") && cfg.decisionBridge && !hasMultiSelect) {
58663
+ const kind = toolName === "ExitPlanMode" ? "plan_approval" : "question";
58664
+ try {
58665
+ const decision = await (cfg.decisionBridge.request ?? requestBridgeDecision)(cfg.decisionBridge.path, { kind, toolName, input: toolInput }, cfg.decisionBridge.timeoutMs);
58666
+ mcpLogger.info(`${toolName} decided via bridge: ${decision.behavior}`);
58667
+ return {
58668
+ behavior: decision.behavior,
58669
+ updatedInput: decision.updatedInput ?? toolInput,
58670
+ message: decision.message
58671
+ };
58672
+ } catch (err) {
58673
+ mcpLogger.warn(`Decision bridge unavailable for ${toolName} (${err}) — legacy fallback`);
58674
+ }
58675
+ }
58439
58676
  if (toolName === "AskUserQuestion") {
58440
58677
  mcpLogger.debug("Auto-allowing AskUserQuestion (question UI is handled by the main bot)");
58441
58678
  return { behavior: "allow", updatedInput: toolInput };
@@ -58522,7 +58759,8 @@ async function handlePermission(toolName, toolInput) {
58522
58759
  getAllowAll: () => allowAllSession,
58523
58760
  setAllowAll: (v) => {
58524
58761
  allowAllSession = v;
58525
- }
58762
+ },
58763
+ decisionBridge: DECISION_BRIDGE_PATH ? { path: DECISION_BRIDGE_PATH, timeoutMs: DECISION_BRIDGE_TIMEOUT_MS } : undefined
58526
58764
  });
58527
58765
  }
58528
58766
  var permissionInputSchema = {
@@ -211,6 +211,7 @@ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions
211
211
  | `SESSION_TIMEOUT_MS` | Idle timeout in milliseconds. Legacy fallback for `limits.sessionTimeoutMinutes`. | `1800000` (30 min) |
212
212
  | `DEBUG` | Enable verbose logging | - |
213
213
  | `CLAUDE_PATH` | Path to the `claude` binary. Overrides the PATH lookup and the common install locations. | `claude` (from PATH) |
214
+ | `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) |
214
215
  | `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+. | - |
215
216
  | `CLAUDE_THREADS_SESSIONS_PATH` | Override the path to the persisted sessions file (default `~/.config/claude-threads/sessions.json`). | - |
216
217
  | `CLAUDE_THREADS_GITHUB_EMAILS_PATH` | Override the path to the GitHub-emails store used for commit attribution. | - |
@@ -218,15 +219,16 @@ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions
218
219
 
219
220
  ### Forwarded to Claude CLI automatically
220
221
 
221
- The bot sets two tuning flags on the Claude child process when they aren't
222
+ The bot sets these tuning flags on the Claude child process when they aren't
222
223
  already present in the bot's environment:
223
224
 
224
225
  | Variable | Effect | Requires |
225
226
  |----------|--------|----------|
226
227
  | `MCP_CONNECTION_NONBLOCKING=true` | Caps `--mcp-config` connects at 5s so a slow MCP server never delays startup | Claude CLI 2.1.89+ |
227
228
  | `ENABLE_PROMPT_CACHING_1H=true` | Opts into 1-hour prompt cache TTL, cutting re-caching cost on long-lived threads | Claude CLI 2.1.108+ |
229
+ | `MCP_TOOL_TIMEOUT=3600000` | Only set when the session has a decision bridge. Without it the CLI abandons a pending MCP permission call after ~2 minutes — far too short for plan approvals and question answers that wait on a human reaction. One hour matches the bridge's own `DECISION_BRIDGE_TIMEOUT_MS` default. Verified against CLI 2.1.223. | — |
228
230
 
229
- Export either with a different value in the bot's own env to disable.
231
+ Export any of them with a different value in the bot's own env to override.
230
232
 
231
233
  ## CLI Options
232
234
 
package/docs/MCP-TOOLS.md CHANGED
@@ -15,7 +15,7 @@ The permission handler the Claude CLI calls before running a tool that needs app
15
15
  | `tool_name` | string | Name of the tool requesting permission. |
16
16
  | `input` | object | The tool's input parameters, shown to the user in the prompt. |
17
17
 
18
- **Guardrail:** Only reactions from users on the session allowlist count. The bot's own reactions (the option emoji it adds) are ignored. If no reaction arrives within `limits.permissionTimeoutSeconds` (default 120), the request is denied.
18
+ **Guardrail:** Only reactions from users on the session allowlist count. The bot's own reactions (the option emoji it adds) are ignored. If no reaction arrives within `limits.permissionTimeoutSeconds` (default 120), the request is denied. Exception: on modern CLIs, `ExitPlanMode` and `AskUserQuestion` don't post a generic prompt at all — they are forwarded over the session's decision bridge to the bot's plan/question reaction UI and wait up to `DECISION_BRIDGE_TIMEOUT_MS` (default 1 hour) for a decision there. To make that hour real, the bot spawns the CLI with `MCP_TOOL_TIMEOUT=3600000` whenever a bridge is configured — without it the CLI abandons a pending permission call after ~2 minutes (verified against CLI 2.1.223). Multi-select questions and any bridge failure fall back to the legacy behavior: generic prompt for plans, auto-allow for questions.
19
19
 
20
20
  ## send_file
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.21.0",
3
+ "version": "1.21.2",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",