botmux 2.87.0 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/dist/adapters/backend/liveness-gate.d.ts +38 -0
- package/dist/adapters/backend/liveness-gate.d.ts.map +1 -0
- package/dist/adapters/backend/liveness-gate.js +53 -0
- package/dist/adapters/backend/liveness-gate.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts +21 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +102 -27
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +20 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +85 -19
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.d.ts +13 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.js +51 -12
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +4 -0
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +6 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +13 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +3 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.js +1 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +165 -5
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +3 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +4 -1
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/session-activity.d.ts +1 -0
- package/dist/core/session-activity.d.ts.map +1 -1
- package/dist/core/session-activity.js +7 -4
- package/dist/core/session-activity.js.map +1 -1
- package/dist/core/session-create.d.ts +62 -0
- package/dist/core/session-create.d.ts.map +1 -0
- package/dist/core/session-create.js +131 -0
- package/dist/core/session-create.js.map +1 -0
- package/dist/core/session-manager.d.ts +40 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +236 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +78 -4
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +11 -5
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +1 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +1 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/session-cleanup.d.ts +30 -0
- package/dist/dashboard/session-cleanup.d.ts.map +1 -0
- package/dist/dashboard/session-cleanup.js +47 -0
- package/dist/dashboard/session-cleanup.js.map +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +20 -5
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +184 -2
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/insights.d.ts.map +1 -1
- package/dist/dashboard/web/insights.js +355 -30
- package/dist/dashboard/web/insights.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +457 -30
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard-web/app.js +767 -640
- package/dist/dashboard-web/style.css +257 -7
- package/dist/dashboard.js +213 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +21 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +21 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +4 -5
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +113 -11
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +3 -2
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +167 -11
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +19 -5
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +9 -2
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/index-daemon.js +5 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/platform/bind.d.ts +2 -0
- package/dist/platform/bind.d.ts.map +1 -0
- package/dist/platform/bind.js +91 -0
- package/dist/platform/bind.js.map +1 -0
- package/dist/platform/binding.d.ts +22 -0
- package/dist/platform/binding.d.ts.map +1 -0
- package/dist/platform/binding.js +33 -0
- package/dist/platform/binding.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +15 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -0
- package/dist/platform/tunnel-client.js +193 -0
- package/dist/platform/tunnel-client.js.map +1 -0
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +1 -0
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/git-worktree.d.ts +10 -0
- package/dist/services/git-worktree.d.ts.map +1 -1
- package/dist/services/git-worktree.js +71 -26
- package/dist/services/git-worktree.js.map +1 -1
- package/dist/services/grant-prefs-store.d.ts +3 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -1
- package/dist/services/grant-prefs-store.js +17 -2
- package/dist/services/grant-prefs-store.js.map +1 -1
- package/dist/services/insight/claude-span-reader.d.ts.map +1 -1
- package/dist/services/insight/claude-span-reader.js +18 -2
- package/dist/services/insight/claude-span-reader.js.map +1 -1
- package/dist/services/insight/codex-span-reader.d.ts.map +1 -1
- package/dist/services/insight/codex-span-reader.js +17 -1
- package/dist/services/insight/codex-span-reader.js.map +1 -1
- package/dist/services/insight/report.d.ts.map +1 -1
- package/dist/services/insight/report.js +48 -6
- package/dist/services/insight/report.js.map +1 -1
- package/dist/services/insight/types.d.ts +9 -0
- package/dist/services/insight/types.d.ts.map +1 -1
- package/dist/services/insight/types.js.map +1 -1
- package/dist/services/worktree-slug-ai.d.ts.map +1 -1
- package/dist/services/worktree-slug-ai.js +3 -1
- package/dist/services/worktree-slug-ai.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/crash-log.d.ts +22 -0
- package/dist/utils/crash-log.d.ts.map +1 -0
- package/dist/utils/crash-log.js +37 -0
- package/dist/utils/crash-log.js.map +1 -0
- package/dist/utils/input-gate.d.ts +10 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +7 -0
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/utils/stdio-epipe-guard.d.ts +39 -0
- package/dist/utils/stdio-epipe-guard.d.ts.map +1 -0
- package/dist/utils/stdio-epipe-guard.js +67 -0
- package/dist/utils/stdio-epipe-guard.js.map +1 -0
- package/dist/worker.js +210 -26
- package/dist/worker.js.map +1 -1
- package/dist/workflows/daemon-spawn.d.ts +2 -0
- package/dist/workflows/daemon-spawn.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
* 7. On 'restart', kills CLI and re-spawns with --resume
|
|
14
14
|
*/
|
|
15
15
|
import { randomBytes } from 'node:crypto';
|
|
16
|
-
import { mkdirSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
|
|
16
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
|
|
17
17
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
18
18
|
import { isAbsolute, join } from 'node:path';
|
|
19
19
|
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
|
|
20
20
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
21
21
|
import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
22
|
-
import { shouldWriteNow } from './utils/input-gate.js';
|
|
22
|
+
import { shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
|
|
23
|
+
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
24
|
+
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
23
25
|
import { mergeQueuedCliInput } from './utils/pending-input-queue.js';
|
|
24
26
|
import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
|
|
25
27
|
import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
|
|
@@ -105,6 +107,17 @@ let lastSpawnEffectiveResume = false;
|
|
|
105
107
|
let lastSpawnEffectiveCliSessionId;
|
|
106
108
|
let idleDetector = null;
|
|
107
109
|
let isTmuxMode = false;
|
|
110
|
+
/** True once a crash diagnostic tmux shell (bmx-diag-<sid>) is live. */
|
|
111
|
+
let crashDiagnosticTmuxParked = false;
|
|
112
|
+
/** True once the daemon told us to stop & park a crash diagnostic (crash loop):
|
|
113
|
+
* the next user message retries the CLI. Distinct from the flag above because
|
|
114
|
+
* retry must still fire even if the tmux park itself failed (no hang). */
|
|
115
|
+
let crashDiagnosticStopped = false;
|
|
116
|
+
/** Exit code/signal of the just-exited CLI, stashed so a deferred park
|
|
117
|
+
* (park_diagnostic IPC) can stamp the captured log even though the park no
|
|
118
|
+
* longer happens inline in onExit. */
|
|
119
|
+
let lastCliExitCode = null;
|
|
120
|
+
let lastCliExitSignal = null;
|
|
108
121
|
/** Adopt-bridge mode using TmuxPipeBackend: not a tmux attach client, all
|
|
109
122
|
* web-terminal updates flow through the shared scrollback fan-out instead
|
|
110
123
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
@@ -164,6 +177,14 @@ let readySignalTimer = null;
|
|
|
164
177
|
* back. The real signal lands within ~ms of the input box rendering, so this is
|
|
165
178
|
* pure insurance against a missing/failed hook — generous but bounded. */
|
|
166
179
|
const READY_SIGNAL_TIMEOUT_MS = 45_000;
|
|
180
|
+
/** Soft fallback for CLIs that never emit an idle/ready signal during startup.
|
|
181
|
+
* Legacy adapters release queued first input here. Adapters that opt into
|
|
182
|
+
* deferFirstPromptTimeoutUntilReady wait for the real readyPattern until the
|
|
183
|
+
* hard cap below. */
|
|
184
|
+
const FIRST_PROMPT_TIMEOUT_MS = 15_000;
|
|
185
|
+
/** Hard cap for startup screens that outlive the soft fallback. Prevents a
|
|
186
|
+
* changed/missing readyPattern from trapping the first queued input forever. */
|
|
187
|
+
const FIRST_PROMPT_HARD_TIMEOUT_MS = 90_000;
|
|
167
188
|
/** Epoch ms of the most recent PTY output — used to settle for quiescence
|
|
168
189
|
* before the first flush (see settleThenFlush). */
|
|
169
190
|
let lastPtyOutputAtMs = 0;
|
|
@@ -2276,6 +2297,8 @@ const MAX_SCROLLBACK = 1_000_000; // chars (~1MB)
|
|
|
2276
2297
|
let scrollback = '';
|
|
2277
2298
|
const WORKFLOW_TRANSCRIPT_MAX = 2_000_000; // chars (~2MB)
|
|
2278
2299
|
const WORKFLOW_OUTPUT_END_MARKER = '</WORKFLOW_OUTPUT>';
|
|
2300
|
+
const CRASH_DIAGNOSTIC_RAW_MAX = 200_000; // enough scrollback for the web terminal without huge temp files
|
|
2301
|
+
const CRASH_LOG_TAIL_MAX = 2_500; // bounded Feishu text payload
|
|
2279
2302
|
let workflowTranscript = '';
|
|
2280
2303
|
let workflowFinalOutputSent = false;
|
|
2281
2304
|
/** Tracks whether the CLI is currently in the alt screen buffer. Updated by
|
|
@@ -2287,6 +2310,97 @@ let workflowFinalOutputSent = false;
|
|
|
2287
2310
|
let altBufferActive = false;
|
|
2288
2311
|
const ALT_ENTER_RE = /\x1b\[\?(1049|1047|47)h/g;
|
|
2289
2312
|
const ALT_EXIT_RE = /\x1b\[\?(1049|1047|47)l/g;
|
|
2313
|
+
function recentTerminalLogTail() {
|
|
2314
|
+
const plain = stripAnsiForLog(tailChars(scrollback, CRASH_DIAGNOSTIC_RAW_MAX));
|
|
2315
|
+
if (!plain)
|
|
2316
|
+
return undefined;
|
|
2317
|
+
return tailChars(plain, CRASH_LOG_TAIL_MAX);
|
|
2318
|
+
}
|
|
2319
|
+
function crashDiagnosticPath() {
|
|
2320
|
+
const dataDir = process.env.SESSION_DATA_DIR;
|
|
2321
|
+
if (!dataDir || !sessionId)
|
|
2322
|
+
return undefined;
|
|
2323
|
+
return join(dataDir, 'crash-diagnostics', `${sessionId}.ansi`);
|
|
2324
|
+
}
|
|
2325
|
+
function destroyCrashDiagnosticTerminal(reason) {
|
|
2326
|
+
// Leaving the stopped-awaiting-retry state regardless of whether a tmux shell
|
|
2327
|
+
// was actually parked (park may have failed); the next retry/close/suspend
|
|
2328
|
+
// funnels through here.
|
|
2329
|
+
crashDiagnosticStopped = false;
|
|
2330
|
+
if (!crashDiagnosticTmuxParked || !sessionId)
|
|
2331
|
+
return;
|
|
2332
|
+
try {
|
|
2333
|
+
TmuxBackend.killSession(TmuxBackend.diagnosticSessionName(sessionId));
|
|
2334
|
+
log(`Crash diagnostic tmux session destroyed (${reason})`);
|
|
2335
|
+
}
|
|
2336
|
+
catch (err) {
|
|
2337
|
+
log(`Crash diagnostic tmux cleanup failed (${reason}): ${err?.message ?? err}`);
|
|
2338
|
+
}
|
|
2339
|
+
// Best-effort: drop the captured .ansi file too so a long-lived daemon does
|
|
2340
|
+
// not accumulate one ~200 KB file per crashed session forever.
|
|
2341
|
+
const path = crashDiagnosticPath();
|
|
2342
|
+
if (path) {
|
|
2343
|
+
try {
|
|
2344
|
+
unlinkSync(path);
|
|
2345
|
+
}
|
|
2346
|
+
catch { /* already gone — benign */ }
|
|
2347
|
+
}
|
|
2348
|
+
crashDiagnosticTmuxParked = false;
|
|
2349
|
+
}
|
|
2350
|
+
function parkCrashDiagnosticTerminal(code, signal) {
|
|
2351
|
+
if (lastInitConfig?.adoptMode || effectiveBackendType !== 'tmux' || !sessionId)
|
|
2352
|
+
return false;
|
|
2353
|
+
const path = crashDiagnosticPath();
|
|
2354
|
+
if (!path)
|
|
2355
|
+
return false;
|
|
2356
|
+
try {
|
|
2357
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2358
|
+
const rawTail = tailChars(scrollback, CRASH_DIAGNOSTIC_RAW_MAX);
|
|
2359
|
+
const header = `[botmux] ${cliName()} exited (code: ${code ?? 'null'}, signal: ${signal ?? 'null'}).\n` +
|
|
2360
|
+
`[botmux] Captured at ${new Date().toISOString()}.\n\n`;
|
|
2361
|
+
writeFileSync(path, header + rawTail);
|
|
2362
|
+
}
|
|
2363
|
+
catch (err) {
|
|
2364
|
+
log(`Crash diagnostic log write failed: ${err?.message ?? err}`);
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
// Park under a DISTINCT name (`bmx-diag-<sid>`), never the live CLI's
|
|
2368
|
+
// `bmx-<sid>` backing-session name. The whole persistent-backend machinery
|
|
2369
|
+
// (restore probe, hasSession reattach, idle-sweep cold-resume, `botmux
|
|
2370
|
+
// resume`) keys off `bmx-<sid>` to mean "the live CLI". Reusing that name for
|
|
2371
|
+
// a bare diagnostic shell makes restore/cold-resume reattach the shell as if
|
|
2372
|
+
// it were the CLI and type the user's next message into raw bash. With a
|
|
2373
|
+
// distinct name, `bmx-<sid>` is correctly absent after the crash, so every
|
|
2374
|
+
// one of those paths sees "no live CLI" and does the right thing; the web
|
|
2375
|
+
// terminal is pointed at the diagnostic name explicitly (see the WS attach).
|
|
2376
|
+
const ok = TmuxBackend.parkDiagnosticSession(TmuxBackend.diagnosticSessionName(sessionId), {
|
|
2377
|
+
cwd: lastInitConfig?.workingDir ?? process.cwd(),
|
|
2378
|
+
cols: renderCols || PTY_COLS,
|
|
2379
|
+
rows: renderRows || PTY_ROWS,
|
|
2380
|
+
contentPath: path,
|
|
2381
|
+
});
|
|
2382
|
+
if (!ok) {
|
|
2383
|
+
// tmux spawn failed after the .ansi was written — drop the orphan file.
|
|
2384
|
+
try {
|
|
2385
|
+
unlinkSync(path);
|
|
2386
|
+
}
|
|
2387
|
+
catch { /* benign */ }
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
crashDiagnosticTmuxParked = true;
|
|
2391
|
+
isTmuxMode = true;
|
|
2392
|
+
isPipeMode = false;
|
|
2393
|
+
isZellijMode = false;
|
|
2394
|
+
// The CLI is gone; stop the screen-update + analyzer loops so a stale
|
|
2395
|
+
// `status='working'` tick can't un-freeze the daemon's frozen crash card.
|
|
2396
|
+
// The web terminal is served by per-client tmux-attach PTYs, not these loops,
|
|
2397
|
+
// so the diagnostic shell stays visible. flushPending's retry path restarts
|
|
2398
|
+
// both when the next message respawns the CLI.
|
|
2399
|
+
stopScreenUpdates();
|
|
2400
|
+
stopScreenAnalyzer();
|
|
2401
|
+
log(`Crash diagnostic tmux session parked at ${TmuxBackend.diagnosticSessionName(sessionId)}`);
|
|
2402
|
+
return true;
|
|
2403
|
+
}
|
|
2290
2404
|
// ─── Screen Analyzer (AI-based TUI prompt detection) ────────────────────────
|
|
2291
2405
|
let screenAnalyzer = null;
|
|
2292
2406
|
/** When true, user messages are queued because a TUI prompt is active */
|
|
@@ -4505,6 +4619,16 @@ function spawnCli(cfg) {
|
|
|
4505
4619
|
backend.onData(onPtyData);
|
|
4506
4620
|
backend.onExit((code, signal) => {
|
|
4507
4621
|
log(`${cliName()} exited (code: ${code}, signal: ${signal})`);
|
|
4622
|
+
const logTail = recentTerminalLogTail();
|
|
4623
|
+
// Don't park a diagnostic shell here: most exits are immediately
|
|
4624
|
+
// auto-restarted by the daemon, so an inline park would just be torn down
|
|
4625
|
+
// again (a wasted tmux session + .ansi write on every restart). Instead
|
|
4626
|
+
// report whether we COULD park; the daemon asks us to (park_diagnostic) only
|
|
4627
|
+
// when it actually gives up restarting (crash loop). Stash the exit reason
|
|
4628
|
+
// for that deferred park.
|
|
4629
|
+
lastCliExitCode = code;
|
|
4630
|
+
lastCliExitSignal = signal;
|
|
4631
|
+
const canParkDiagnostic = !lastInitConfig?.adoptMode && effectiveBackendType === 'tmux' && !!sessionId;
|
|
4508
4632
|
// Inputs written but not yet consumed (no idle since the write) die with
|
|
4509
4633
|
// the CLI — codex crashing mid-submit never records them, and the fresh
|
|
4510
4634
|
// respawn comes up empty. Stash them so the next spawnCli re-queues and
|
|
@@ -4515,38 +4639,56 @@ function spawnCli(cfg) {
|
|
|
4515
4639
|
}
|
|
4516
4640
|
backend = null;
|
|
4517
4641
|
isPromptReady = false;
|
|
4518
|
-
send({ type: 'claude_exit', code, signal });
|
|
4642
|
+
send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic });
|
|
4519
4643
|
});
|
|
4520
4644
|
if (isPipeMode && backend && 'isReattach' in backend && backend.isReattach) {
|
|
4521
4645
|
log(`Re-attached to existing ${effectiveBackendType} session via pipe backend: ${persistentSessionName}`);
|
|
4522
4646
|
seedBackendScreen(`${effectiveBackendType} reattach`, backend);
|
|
4523
4647
|
scheduleReattachIdleProbe(`${effectiveBackendType} reattach`, backend);
|
|
4524
4648
|
}
|
|
4525
|
-
// Fallback: if the CLI takes too long to show its prompt (e.g. slow
|
|
4526
|
-
//
|
|
4527
|
-
//
|
|
4528
|
-
//
|
|
4529
|
-
//
|
|
4530
|
-
//
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4649
|
+
// Fallback: if the CLI takes too long to show its prompt (e.g. slow plugin
|
|
4650
|
+
// init, or a spinner blocks the idle detector), unblock screen updates AND
|
|
4651
|
+
// deliver any queued prompts so the first user message isn't stranded until
|
|
4652
|
+
// the second message arrives. Some adapters opt into deferring the soft
|
|
4653
|
+
// fallback until readyPattern, but still get a hard cap below.
|
|
4654
|
+
// markNewTurn() sets a clean baseline at the current cursor position so only
|
|
4655
|
+
// content written *after* this point appears in the card.
|
|
4656
|
+
const firstPromptBackend = backend;
|
|
4657
|
+
const releaseFirstPromptTimeout = (elapsedMs, forced) => {
|
|
4658
|
+
if (!awaitingFirstPrompt || backend !== firstPromptBackend)
|
|
4659
|
+
return;
|
|
4660
|
+
if (!shouldReleaseFirstPromptTimeout({
|
|
4661
|
+
deferFirstPromptTimeoutUntilReady: cliAdapter?.deferFirstPromptTimeoutUntilReady === true,
|
|
4662
|
+
hasReadyPattern: !!cliAdapter?.readyPattern,
|
|
4663
|
+
elapsedMs,
|
|
4664
|
+
hardTimeoutMs: FIRST_PROMPT_HARD_TIMEOUT_MS,
|
|
4665
|
+
})) {
|
|
4666
|
+
const hardWaitMs = Math.max(0, FIRST_PROMPT_HARD_TIMEOUT_MS - elapsedMs);
|
|
4667
|
+
log(`First prompt timeout — ${cliName()} still waiting for readyPattern before flushing queued messages`);
|
|
4668
|
+
const hardTimer = setTimeout(() => releaseFirstPromptTimeout(FIRST_PROMPT_HARD_TIMEOUT_MS, true), hardWaitMs);
|
|
4669
|
+
hardTimer.unref?.();
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
awaitingFirstPrompt = false;
|
|
4673
|
+
renderer?.markNewTurn();
|
|
4674
|
+
log(forced
|
|
4675
|
+
? `WARN First prompt hard timeout — ${cliName()} readyPattern did not arrive; forcing queued message flush`
|
|
4676
|
+
: 'First prompt timeout — enabling screen updates and flushing queued messages');
|
|
4677
|
+
if (backend && cliAdapter?.busyPattern && probeBusyPatternIdle(`${cliName()} first-prompt-timeout`, backend)) {
|
|
4678
|
+
return;
|
|
4546
4679
|
}
|
|
4547
|
-
|
|
4680
|
+
// For type-ahead adapters (Codex/CoCo/Claude/TraeX) the TUI is usually booted
|
|
4681
|
+
// enough to park input even if the idle detector hasn't fired yet. Directly
|
|
4682
|
+
// invoking markPromptReady() would claim the CLI is idle while it's still
|
|
4683
|
+
// mid-boot, so flushPending() alone is safer — it respects typeAheadAllowed
|
|
4684
|
+
// and drains pendingMessages now.
|
|
4685
|
+
if (cliAdapter?.supportsTypeAhead)
|
|
4686
|
+
flushPending();
|
|
4687
|
+
};
|
|
4688
|
+
setTimeout(() => releaseFirstPromptTimeout(FIRST_PROMPT_TIMEOUT_MS, false), FIRST_PROMPT_TIMEOUT_MS);
|
|
4548
4689
|
}
|
|
4549
4690
|
function killCli() {
|
|
4691
|
+
destroyCrashDiagnosticTerminal('killCli');
|
|
4550
4692
|
idleDetector?.dispose();
|
|
4551
4693
|
idleDetector = null;
|
|
4552
4694
|
stopReattachIdleProbe();
|
|
@@ -4646,7 +4788,13 @@ function startWebServer(host, preferredPort) {
|
|
|
4646
4788
|
// byte-for-byte (empty, separators, etc.) are not retransmitted, so
|
|
4647
4789
|
// the earlier frame "bleeds through" — visible as a second
|
|
4648
4790
|
// banner/prompt stacked above the new layout when scrolling up.
|
|
4649
|
-
|
|
4791
|
+
// While a crash diagnostic shell is parked it lives under bmx-diag-<sid>
|
|
4792
|
+
// (not the live CLI's bmx-<sid>), so attach there to surface the startup
|
|
4793
|
+
// error; otherwise attach the normal backing session.
|
|
4794
|
+
const tmuxTarget = lastInitConfig?.adoptTmuxTarget
|
|
4795
|
+
?? (crashDiagnosticTmuxParked
|
|
4796
|
+
? TmuxBackend.diagnosticSessionName(sessionId)
|
|
4797
|
+
: TmuxBackend.sessionName(sessionId));
|
|
4650
4798
|
let cp = null;
|
|
4651
4799
|
const pendingInput = [];
|
|
4652
4800
|
const startAttach = (cols, rows) => {
|
|
@@ -5202,6 +5350,16 @@ process.on('message', async (raw) => {
|
|
|
5202
5350
|
const content = msg.content;
|
|
5203
5351
|
currentBotmuxTurnId = msg.turnId;
|
|
5204
5352
|
writeCliPidMarker();
|
|
5353
|
+
if (!backend && crashDiagnosticStopped && lastInitConfig && !lastInitConfig.adoptMode) {
|
|
5354
|
+
log('Message received after crash-loop stop; retrying CLI start');
|
|
5355
|
+
destroyCrashDiagnosticTerminal('retry after message');
|
|
5356
|
+
stopScreenAnalyzer();
|
|
5357
|
+
stopScreenUpdates();
|
|
5358
|
+
awaitingFirstPrompt = true;
|
|
5359
|
+
startScreenUpdates();
|
|
5360
|
+
startScreenAnalyzer();
|
|
5361
|
+
spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
|
|
5362
|
+
}
|
|
5205
5363
|
if (lastInitConfig?.adoptMode) {
|
|
5206
5364
|
// Bridge mode: capture transcript baseline BEFORE writing to the pane,
|
|
5207
5365
|
// so any assistant uuids appended after this point are attributed to
|
|
@@ -5332,6 +5490,16 @@ process.on('message', async (raw) => {
|
|
|
5332
5490
|
}
|
|
5333
5491
|
break;
|
|
5334
5492
|
}
|
|
5493
|
+
case 'park_diagnostic': {
|
|
5494
|
+
// The daemon gave up auto-restarting (crash loop) and wants the last
|
|
5495
|
+
// terminal output preserved. Park the diagnostic shell now — deferred from
|
|
5496
|
+
// onExit so transient (auto-restarted) exits never pay for it. Mark the
|
|
5497
|
+
// stopped state even if the tmux park fails, so the next message still
|
|
5498
|
+
// retries the CLI (no hang) rather than writing into a dead pane.
|
|
5499
|
+
parkCrashDiagnosticTerminal(lastCliExitCode, lastCliExitSignal);
|
|
5500
|
+
crashDiagnosticStopped = true;
|
|
5501
|
+
break;
|
|
5502
|
+
}
|
|
5335
5503
|
case 'restart': {
|
|
5336
5504
|
if (lastInitConfig?.adoptMode) {
|
|
5337
5505
|
log('Restart ignored in adopt mode');
|
|
@@ -5436,6 +5604,11 @@ process.on('message', async (raw) => {
|
|
|
5436
5604
|
log('Suspend requested');
|
|
5437
5605
|
stopScreenshotLoop();
|
|
5438
5606
|
stopBridgeWatcher();
|
|
5607
|
+
// A parked crash diagnostic shell has backend===null, so the
|
|
5608
|
+
// destroySession/kill below is a no-op and would otherwise leak the
|
|
5609
|
+
// bmx-diag-<sid> session. Tear it down explicitly. (The session then
|
|
5610
|
+
// cold-resumes a FRESH CLI on the next message — bmx-<sid> is absent.)
|
|
5611
|
+
destroyCrashDiagnosticTerminal('suspend');
|
|
5439
5612
|
// Free the CLI's memory, not just the worker's: destroySession kills the
|
|
5440
5613
|
// backing tmux/herdr/zellij session AND the CLI process inside it (kill()
|
|
5441
5614
|
// would only detach the pty viewer and leave the CLI running in the
|
|
@@ -5556,8 +5729,17 @@ function teardownSandboxBestEffort() {
|
|
|
5556
5729
|
catch { /* */ }
|
|
5557
5730
|
sandboxCleanup = null;
|
|
5558
5731
|
}
|
|
5732
|
+
// Under pm2 the worker's stdout/stderr are pipes; a broken pipe (e.g. log
|
|
5733
|
+
// streaming detaches) would otherwise reach the uncaughtException handler below
|
|
5734
|
+
// and process.exit(1), killing a live session over a dropped log write. Install
|
|
5735
|
+
// the guard before any further stdout writes (log() writes to process.stdout).
|
|
5736
|
+
installStdioEpipeGuard();
|
|
5559
5737
|
process.on('exit', () => { teardownSandboxBestEffort(); });
|
|
5560
5738
|
process.on('uncaughtException', (err) => {
|
|
5739
|
+
// A broken pipe on stdout/stderr (or any socket) must not tear down a live
|
|
5740
|
+
// session — the stdio guard handles those it can; this is the backstop.
|
|
5741
|
+
if (isIgnorableStreamError(err))
|
|
5742
|
+
return;
|
|
5561
5743
|
try {
|
|
5562
5744
|
log(`Uncaught exception — tearing down sandbox before exit: ${err?.stack ?? err}`);
|
|
5563
5745
|
}
|
|
@@ -5570,6 +5752,8 @@ process.on('uncaughtException', (err) => {
|
|
|
5570
5752
|
process.exit(1);
|
|
5571
5753
|
});
|
|
5572
5754
|
process.on('unhandledRejection', (reason) => {
|
|
5755
|
+
if (isIgnorableStreamError(reason))
|
|
5756
|
+
return;
|
|
5573
5757
|
try {
|
|
5574
5758
|
log(`Unhandled rejection — tearing down sandbox before exit: ${reason?.stack ?? reason}`);
|
|
5575
5759
|
}
|