botmux 2.86.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 +27 -0
- package/README.md +27 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +12 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- 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/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +4 -1
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +22 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +124 -36
- 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 +86 -20
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +9 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +1 -1
- package/dist/adapters/backend/zellij-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/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +8 -5
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +25 -3
- package/dist/adapters/cli/shared-hints.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 +23 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +9 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +368 -16
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +7 -5
- 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 +210 -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/maintenance.d.ts +18 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +58 -30
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/per-bot-env.d.ts +38 -0
- package/dist/core/per-bot-env.d.ts.map +1 -0
- package/dist/core/per-bot-env.js +79 -0
- package/dist/core/per-bot-env.js.map +1 -0
- 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 +43 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +306 -8
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/skills/discovery.d.ts +7 -0
- package/dist/core/skills/discovery.d.ts.map +1 -1
- package/dist/core/skills/discovery.js +51 -0
- package/dist/core/skills/discovery.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +5 -2
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/update-check.d.ts +67 -0
- package/dist/core/update-check.d.ts.map +1 -0
- package/dist/core/update-check.js +159 -0
- package/dist/core/update-check.js.map +1 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +82 -4
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +40 -11
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +2 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +2 -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/skill-install-request.d.ts +10 -0
- package/dist/dashboard/skill-install-request.d.ts.map +1 -1
- package/dist/dashboard/skill-install-request.js +17 -0
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +29 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +82 -6
- 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 +312 -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/settings.d.ts.map +1 -1
- package/dist/dashboard/web/settings.js +289 -0
- package/dist/dashboard/web/settings.js.map +1 -1
- package/dist/dashboard/web/skills.d.ts.map +1 -1
- package/dist/dashboard/web/skills.js +183 -10
- package/dist/dashboard/web/skills.js.map +1 -1
- package/dist/dashboard/web/whiteboards.d.ts +2 -0
- package/dist/dashboard/web/whiteboards.d.ts.map +1 -0
- package/dist/dashboard/web/whiteboards.js +267 -0
- package/dist/dashboard/web/whiteboards.js.map +1 -0
- package/dist/dashboard-web/app.js +879 -591
- package/dist/dashboard-web/index.html +1 -0
- package/dist/dashboard-web/style.css +528 -8
- package/dist/dashboard.js +472 -5
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +6 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +12 -0
- package/dist/global-config.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 +174 -14
- 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 +20 -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/resumable-session-discovery.d.ts.map +1 -1
- package/dist/services/resumable-session-discovery.js +21 -4
- package/dist/services/resumable-session-discovery.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts +1 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +32 -0
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/whiteboard-store.d.ts +67 -0
- package/dist/services/whiteboard-store.d.ts.map +1 -0
- package/dist/services/whiteboard-store.js +450 -0
- package/dist/services/whiteboard-store.js.map +1 -0
- 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/setup/bot-config-editor.d.ts +8 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +9 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +118 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +15 -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/install-diagnostics.d.ts +52 -0
- package/dist/utils/install-diagnostics.d.ts.map +1 -0
- package/dist/utils/install-diagnostics.js +155 -0
- package/dist/utils/install-diagnostics.js.map +1 -0
- package/dist/utils/install-info.d.ts +4 -0
- package/dist/utils/install-info.d.ts.map +1 -1
- package/dist/utils/install-info.js +6 -0
- package/dist/utils/install-info.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 +222 -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
|
@@ -40,6 +40,12 @@ export function botmuxVersion() {
|
|
|
40
40
|
export function botmuxCliEntry() {
|
|
41
41
|
return join(packageRoot(), 'dist', 'cli.js');
|
|
42
42
|
}
|
|
43
|
+
/** Absolute path to this install's root (the dir holding package.json). For a
|
|
44
|
+
* source checkout this is the git working tree — used to derive a real version
|
|
45
|
+
* via `git describe` when package.json is the unbuilt 0.0.0. */
|
|
46
|
+
export function botmuxInstallRoot() {
|
|
47
|
+
return packageRoot();
|
|
48
|
+
}
|
|
43
49
|
/** Walk up from this module to the nearest dir containing package.json. */
|
|
44
50
|
function packageRoot() {
|
|
45
51
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-info.js","sourceRoot":"","sources":["../../src/utils/install-info.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,iFAAiF;AACjF,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAO,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,IAAI,MAA2B,CAAC;AAEhC,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB;IAC/B,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,GAAG,mBAAmB,CAAC,WAAW,EAAE,CAAC,CAAC;IACtE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;yEAEyE;AACzE,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACnF,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;+CAE+C;AAC/C,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED,2EAA2E;AAC3E,SAAS,WAAW;IAClB,IAAI,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QACtD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
1
|
+
{"version":3,"file":"install-info.js","sourceRoot":"","sources":["../../src/utils/install-info.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,iFAAiF;AACjF,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAO,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,IAAI,MAA2B,CAAC;AAEhC,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB;IAC/B,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,GAAG,mBAAmB,CAAC,WAAW,EAAE,CAAC,CAAC;IACtE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;yEAEyE;AACzE,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACnF,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;+CAE+C;AAC/C,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED;;iEAEiE;AACjE,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,2EAA2E;AAC3E,SAAS,WAAW;IAClB,IAAI,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QACtD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suppress EPIPE-class crashes on process.stdout / process.stderr.
|
|
3
|
+
*
|
|
4
|
+
* When botmux runs under pm2, the daemon's and each worker's stdout/stderr are
|
|
5
|
+
* pipes to the pm2 God daemon (→ out_file / error_file). If the reader end
|
|
6
|
+
* detaches — pm2 log streaming stops, the God daemon restarts, `botmux logs`
|
|
7
|
+
* is piped to a command that exits early (e.g. `| head`), or the terminal
|
|
8
|
+
* closes mid-stream — the next write fails with EPIPE. Because a piped
|
|
9
|
+
* stdout/stderr is a Socket, the failed write surfaces as an 'error' event;
|
|
10
|
+
* with no listener Node escalates it to an uncaughtException.
|
|
11
|
+
*
|
|
12
|
+
* That is fatal for botmux: the daemon registers no global uncaughtException
|
|
13
|
+
* handler (so a broken pipe would kill the whole daemon), and the worker's
|
|
14
|
+
* handler calls process.exit(1) on any uncaught error (so a broken pipe would
|
|
15
|
+
* take down a live CLI session). The real logs are captured by pm2's out/err
|
|
16
|
+
* files regardless, so the correct behavior is to drop the failed write
|
|
17
|
+
* silently and keep running rather than crash.
|
|
18
|
+
*
|
|
19
|
+
* Attaching an 'error' listener that ignores the broken-pipe family achieves
|
|
20
|
+
* exactly that. Genuinely unexpected stream errors are re-thrown so they still
|
|
21
|
+
* surface (i.e. behavior for non-broken-pipe errors is unchanged).
|
|
22
|
+
*/
|
|
23
|
+
/** True if `err` is a broken-pipe-class stream error that is safe to swallow. */
|
|
24
|
+
export declare function isIgnorableStreamError(err: NodeJS.ErrnoException | null | undefined): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Attach a broken-pipe-tolerant 'error' listener to a single stream. Exported
|
|
27
|
+
* for unit testing with an injected EventEmitter.
|
|
28
|
+
*/
|
|
29
|
+
export declare function attachStreamErrorGuard(stream: NodeJS.EventEmitter): void;
|
|
30
|
+
/**
|
|
31
|
+
* Install the guard on process.stdout/stderr. Idempotent — safe to call from
|
|
32
|
+
* multiple entry points (daemon, worker). Returns true if it installed this
|
|
33
|
+
* call, false if it was already installed.
|
|
34
|
+
*
|
|
35
|
+
* `streams` defaults to the real process streams; it is parameterized only so
|
|
36
|
+
* unit tests can inject fakes without touching the test runner's own stdio.
|
|
37
|
+
*/
|
|
38
|
+
export declare function installStdioEpipeGuard(streams?: NodeJS.EventEmitter[]): boolean;
|
|
39
|
+
//# sourceMappingURL=stdio-epipe-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-epipe-guard.d.ts","sourceRoot":"","sources":["../../src/utils/stdio-epipe-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAeH,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE7F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,GAAG,IAAI,CAOxE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,MAAM,CAAC,YAAY,EAAqC,GAChE,OAAO,CAKT"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suppress EPIPE-class crashes on process.stdout / process.stderr.
|
|
3
|
+
*
|
|
4
|
+
* When botmux runs under pm2, the daemon's and each worker's stdout/stderr are
|
|
5
|
+
* pipes to the pm2 God daemon (→ out_file / error_file). If the reader end
|
|
6
|
+
* detaches — pm2 log streaming stops, the God daemon restarts, `botmux logs`
|
|
7
|
+
* is piped to a command that exits early (e.g. `| head`), or the terminal
|
|
8
|
+
* closes mid-stream — the next write fails with EPIPE. Because a piped
|
|
9
|
+
* stdout/stderr is a Socket, the failed write surfaces as an 'error' event;
|
|
10
|
+
* with no listener Node escalates it to an uncaughtException.
|
|
11
|
+
*
|
|
12
|
+
* That is fatal for botmux: the daemon registers no global uncaughtException
|
|
13
|
+
* handler (so a broken pipe would kill the whole daemon), and the worker's
|
|
14
|
+
* handler calls process.exit(1) on any uncaught error (so a broken pipe would
|
|
15
|
+
* take down a live CLI session). The real logs are captured by pm2's out/err
|
|
16
|
+
* files regardless, so the correct behavior is to drop the failed write
|
|
17
|
+
* silently and keep running rather than crash.
|
|
18
|
+
*
|
|
19
|
+
* Attaching an 'error' listener that ignores the broken-pipe family achieves
|
|
20
|
+
* exactly that. Genuinely unexpected stream errors are re-thrown so they still
|
|
21
|
+
* surface (i.e. behavior for non-broken-pipe errors is unchanged).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Error codes that mean "the other end of stdout/stderr went away". These are
|
|
25
|
+
* benign for a long-lived process whose real logs live in pm2's files.
|
|
26
|
+
*/
|
|
27
|
+
const IGNORED_STREAM_ERROR_CODES = new Set([
|
|
28
|
+
'EPIPE',
|
|
29
|
+
'ERR_STREAM_DESTROYED',
|
|
30
|
+
'ECONNRESET',
|
|
31
|
+
'EOF',
|
|
32
|
+
]);
|
|
33
|
+
let installed = false;
|
|
34
|
+
/** True if `err` is a broken-pipe-class stream error that is safe to swallow. */
|
|
35
|
+
export function isIgnorableStreamError(err) {
|
|
36
|
+
return !!err && IGNORED_STREAM_ERROR_CODES.has(err.code ?? '');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Attach a broken-pipe-tolerant 'error' listener to a single stream. Exported
|
|
40
|
+
* for unit testing with an injected EventEmitter.
|
|
41
|
+
*/
|
|
42
|
+
export function attachStreamErrorGuard(stream) {
|
|
43
|
+
stream.on('error', (err) => {
|
|
44
|
+
if (isIgnorableStreamError(err))
|
|
45
|
+
return;
|
|
46
|
+
// Not a broken pipe — re-throw so genuinely unexpected stream errors still
|
|
47
|
+
// surface (same outcome as having no listener at all for these codes).
|
|
48
|
+
throw err;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Install the guard on process.stdout/stderr. Idempotent — safe to call from
|
|
53
|
+
* multiple entry points (daemon, worker). Returns true if it installed this
|
|
54
|
+
* call, false if it was already installed.
|
|
55
|
+
*
|
|
56
|
+
* `streams` defaults to the real process streams; it is parameterized only so
|
|
57
|
+
* unit tests can inject fakes without touching the test runner's own stdio.
|
|
58
|
+
*/
|
|
59
|
+
export function installStdioEpipeGuard(streams = [process.stdout, process.stderr]) {
|
|
60
|
+
if (installed)
|
|
61
|
+
return false;
|
|
62
|
+
installed = true;
|
|
63
|
+
for (const stream of streams)
|
|
64
|
+
attachStreamErrorGuard(stream);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=stdio-epipe-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-epipe-guard.js","sourceRoot":"","sources":["../../src/utils/stdio-epipe-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;GAGG;AACH,MAAM,0BAA0B,GAAG,IAAI,GAAG,CAAC;IACzC,OAAO;IACP,sBAAsB;IACtB,YAAY;IACZ,KAAK;CACN,CAAC,CAAC;AAEH,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,GAA6C;IAClF,OAAO,CAAC,CAAC,GAAG,IAAI,0BAA0B,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA2B;IAChE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;QAChD,IAAI,sBAAsB,CAAC,GAAG,CAAC;YAAE,OAAO;QACxC,2EAA2E;QAC3E,uEAAuE;QACvE,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAiC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC;IAEjE,IAAI,SAAS;QAAE,OAAO,KAAK,CAAC;IAC5B,SAAS,GAAG,IAAI,CAAC;IACjB,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/worker.js
CHANGED
|
@@ -13,16 +13,19 @@
|
|
|
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';
|
|
28
|
+
import { sanitizePerBotEnv } from './core/per-bot-env.js';
|
|
26
29
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
27
30
|
import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, shouldHealAbsentBaseline, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
|
|
28
31
|
import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
|
|
@@ -104,6 +107,17 @@ let lastSpawnEffectiveResume = false;
|
|
|
104
107
|
let lastSpawnEffectiveCliSessionId;
|
|
105
108
|
let idleDetector = null;
|
|
106
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;
|
|
107
121
|
/** Adopt-bridge mode using TmuxPipeBackend: not a tmux attach client, all
|
|
108
122
|
* web-terminal updates flow through the shared scrollback fan-out instead
|
|
109
123
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
@@ -163,6 +177,14 @@ let readySignalTimer = null;
|
|
|
163
177
|
* back. The real signal lands within ~ms of the input box rendering, so this is
|
|
164
178
|
* pure insurance against a missing/failed hook — generous but bounded. */
|
|
165
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;
|
|
166
188
|
/** Epoch ms of the most recent PTY output — used to settle for quiescence
|
|
167
189
|
* before the first flush (see settleThenFlush). */
|
|
168
190
|
let lastPtyOutputAtMs = 0;
|
|
@@ -2275,6 +2297,8 @@ const MAX_SCROLLBACK = 1_000_000; // chars (~1MB)
|
|
|
2275
2297
|
let scrollback = '';
|
|
2276
2298
|
const WORKFLOW_TRANSCRIPT_MAX = 2_000_000; // chars (~2MB)
|
|
2277
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
|
|
2278
2302
|
let workflowTranscript = '';
|
|
2279
2303
|
let workflowFinalOutputSent = false;
|
|
2280
2304
|
/** Tracks whether the CLI is currently in the alt screen buffer. Updated by
|
|
@@ -2286,6 +2310,97 @@ let workflowFinalOutputSent = false;
|
|
|
2286
2310
|
let altBufferActive = false;
|
|
2287
2311
|
const ALT_ENTER_RE = /\x1b\[\?(1049|1047|47)h/g;
|
|
2288
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
|
+
}
|
|
2289
2404
|
// ─── Screen Analyzer (AI-based TUI prompt detection) ────────────────────────
|
|
2290
2405
|
let screenAnalyzer = null;
|
|
2291
2406
|
/** When true, user messages are queued because a TUI prompt is active */
|
|
@@ -4063,6 +4178,16 @@ function spawnCli(cfg) {
|
|
|
4063
4178
|
// them past the server's global env.
|
|
4064
4179
|
if (cliAdapter.spawnEnv)
|
|
4065
4180
|
Object.assign(childEnv, cliAdapter.spawnEnv);
|
|
4181
|
+
// Per-bot env (bots.json `env`): extra vars for THIS bot's CLI only — e.g.
|
|
4182
|
+
// ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN to run a bot on GLM/a 3rd-party
|
|
4183
|
+
// provider, an HTTPS_PROXY, or a CLI feature flag. Passed as injectEnv (NOT
|
|
4184
|
+
// merged into childEnv) so the tmux/zellij backends inject it via the per-pane
|
|
4185
|
+
// `/usr/bin/env` prefix and never into the shared backing-server global env,
|
|
4186
|
+
// keeping it from leaking across bots. Re-sanitized here (crossed IPC).
|
|
4187
|
+
const perBotInjectEnv = sanitizePerBotEnv(cfg.env);
|
|
4188
|
+
const perBotInjectKeys = Object.keys(perBotInjectEnv);
|
|
4189
|
+
if (perBotInjectKeys.length)
|
|
4190
|
+
log(`Injecting ${perBotInjectKeys.length} per-bot env var(s): ${perBotInjectKeys.join(', ')}`);
|
|
4066
4191
|
// ── File sandbox (oncall): wrap the CLI in bwrap so it can only touch a
|
|
4067
4192
|
// per-session project copy + de-identified config. The agent's `botmux send`
|
|
4068
4193
|
// routes through a daemon-side outbox watcher (creds never enter the sandbox).
|
|
@@ -4230,6 +4355,7 @@ function spawnCli(cfg) {
|
|
|
4230
4355
|
cols: PTY_COLS,
|
|
4231
4356
|
rows: PTY_ROWS,
|
|
4232
4357
|
env: childEnv,
|
|
4358
|
+
injectEnv: perBotInjectKeys.length ? perBotInjectEnv : undefined,
|
|
4233
4359
|
});
|
|
4234
4360
|
// Write CLI PID marker so agent-facing subcommands (`botmux send`, etc.)
|
|
4235
4361
|
// can verify they were spawned inside a botmux session by walking the
|
|
@@ -4493,6 +4619,16 @@ function spawnCli(cfg) {
|
|
|
4493
4619
|
backend.onData(onPtyData);
|
|
4494
4620
|
backend.onExit((code, signal) => {
|
|
4495
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;
|
|
4496
4632
|
// Inputs written but not yet consumed (no idle since the write) die with
|
|
4497
4633
|
// the CLI — codex crashing mid-submit never records them, and the fresh
|
|
4498
4634
|
// respawn comes up empty. Stash them so the next spawnCli re-queues and
|
|
@@ -4503,38 +4639,56 @@ function spawnCli(cfg) {
|
|
|
4503
4639
|
}
|
|
4504
4640
|
backend = null;
|
|
4505
4641
|
isPromptReady = false;
|
|
4506
|
-
send({ type: 'claude_exit', code, signal });
|
|
4642
|
+
send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic });
|
|
4507
4643
|
});
|
|
4508
4644
|
if (isPipeMode && backend && 'isReattach' in backend && backend.isReattach) {
|
|
4509
4645
|
log(`Re-attached to existing ${effectiveBackendType} session via pipe backend: ${persistentSessionName}`);
|
|
4510
4646
|
seedBackendScreen(`${effectiveBackendType} reattach`, backend);
|
|
4511
4647
|
scheduleReattachIdleProbe(`${effectiveBackendType} reattach`, backend);
|
|
4512
4648
|
}
|
|
4513
|
-
// Fallback: if the CLI takes too long to show its prompt (e.g. slow
|
|
4514
|
-
//
|
|
4515
|
-
//
|
|
4516
|
-
//
|
|
4517
|
-
//
|
|
4518
|
-
//
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
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;
|
|
4534
4679
|
}
|
|
4535
|
-
|
|
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);
|
|
4536
4689
|
}
|
|
4537
4690
|
function killCli() {
|
|
4691
|
+
destroyCrashDiagnosticTerminal('killCli');
|
|
4538
4692
|
idleDetector?.dispose();
|
|
4539
4693
|
idleDetector = null;
|
|
4540
4694
|
stopReattachIdleProbe();
|
|
@@ -4634,7 +4788,13 @@ function startWebServer(host, preferredPort) {
|
|
|
4634
4788
|
// byte-for-byte (empty, separators, etc.) are not retransmitted, so
|
|
4635
4789
|
// the earlier frame "bleeds through" — visible as a second
|
|
4636
4790
|
// banner/prompt stacked above the new layout when scrolling up.
|
|
4637
|
-
|
|
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));
|
|
4638
4798
|
let cp = null;
|
|
4639
4799
|
const pendingInput = [];
|
|
4640
4800
|
const startAttach = (cols, rows) => {
|
|
@@ -5190,6 +5350,16 @@ process.on('message', async (raw) => {
|
|
|
5190
5350
|
const content = msg.content;
|
|
5191
5351
|
currentBotmuxTurnId = msg.turnId;
|
|
5192
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
|
+
}
|
|
5193
5363
|
if (lastInitConfig?.adoptMode) {
|
|
5194
5364
|
// Bridge mode: capture transcript baseline BEFORE writing to the pane,
|
|
5195
5365
|
// so any assistant uuids appended after this point are attributed to
|
|
@@ -5320,6 +5490,16 @@ process.on('message', async (raw) => {
|
|
|
5320
5490
|
}
|
|
5321
5491
|
break;
|
|
5322
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
|
+
}
|
|
5323
5503
|
case 'restart': {
|
|
5324
5504
|
if (lastInitConfig?.adoptMode) {
|
|
5325
5505
|
log('Restart ignored in adopt mode');
|
|
@@ -5424,6 +5604,11 @@ process.on('message', async (raw) => {
|
|
|
5424
5604
|
log('Suspend requested');
|
|
5425
5605
|
stopScreenshotLoop();
|
|
5426
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');
|
|
5427
5612
|
// Free the CLI's memory, not just the worker's: destroySession kills the
|
|
5428
5613
|
// backing tmux/herdr/zellij session AND the CLI process inside it (kill()
|
|
5429
5614
|
// would only detach the pty viewer and leave the CLI running in the
|
|
@@ -5544,8 +5729,17 @@ function teardownSandboxBestEffort() {
|
|
|
5544
5729
|
catch { /* */ }
|
|
5545
5730
|
sandboxCleanup = null;
|
|
5546
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();
|
|
5547
5737
|
process.on('exit', () => { teardownSandboxBestEffort(); });
|
|
5548
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;
|
|
5549
5743
|
try {
|
|
5550
5744
|
log(`Uncaught exception — tearing down sandbox before exit: ${err?.stack ?? err}`);
|
|
5551
5745
|
}
|
|
@@ -5558,6 +5752,8 @@ process.on('uncaughtException', (err) => {
|
|
|
5558
5752
|
process.exit(1);
|
|
5559
5753
|
});
|
|
5560
5754
|
process.on('unhandledRejection', (reason) => {
|
|
5755
|
+
if (isIgnorableStreamError(reason))
|
|
5756
|
+
return;
|
|
5561
5757
|
try {
|
|
5562
5758
|
log(`Unhandled rejection — tearing down sandbox before exit: ${reason?.stack ?? reason}`);
|
|
5563
5759
|
}
|