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 +14 -0
- package/dist/index.js +325 -61
- package/dist/mcp/mcp-server.js +252 -14
- package/docs/CONFIGURATION.md +4 -2
- package/docs/MCP-TOOLS.md +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
14207
|
-
|
|
14206
|
+
join11(etc, name, "config"),
|
|
14207
|
+
join11(etc, name + "rc")
|
|
14208
14208
|
].forEach(addConfigFile);
|
|
14209
14209
|
if (home)
|
|
14210
14210
|
[
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
58544
|
-
import { tmpdir as
|
|
58545
|
-
import { join as
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
62098
|
-
import { join as
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
68237
|
-
session.claude
|
|
68238
|
-
|
|
68239
|
-
|
|
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 =
|
|
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
|
-
|
|
68330
|
-
session.claude
|
|
68331
|
-
|
|
68332
|
-
|
|
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
|
-
|
|
69234
|
-
session.claude
|
|
69235
|
-
|
|
69236
|
-
|
|
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
|
-
|
|
69433
|
+
newClaude.start();
|
|
69434
|
+
if (isSessionRestarting(session)) {
|
|
69435
|
+
transitionTo(session, "active");
|
|
69436
|
+
}
|
|
69239
69437
|
return true;
|
|
69240
69438
|
} catch (err) {
|
|
69241
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
82854
|
+
const bunRoot = _env.BUN_INSTALL || (home ? join12(home, ".bun") : null);
|
|
82591
82855
|
if (bunRoot) {
|
|
82592
|
-
const bunBin =
|
|
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 =
|
|
82863
|
+
const candidate = join12(dir, name);
|
|
82600
82864
|
if (_existsSync(candidate) && _isFileExecutable(candidate)) {
|
|
82601
82865
|
return candidate;
|
|
82602
82866
|
}
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
55917
|
-
const path =
|
|
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 =
|
|
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
|
|
57222
|
+
import { join as join6 } from "path";
|
|
57003
57223
|
var log18 = createLogger("gh-emails");
|
|
57004
|
-
var DEFAULT_CONFIG_DIR =
|
|
57005
|
-
var DEFAULT_FILE =
|
|
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 = {
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
|
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
|
|
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