aicodeman 1.2.2 → 1.3.1

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.
Files changed (227) hide show
  1. package/README.md +264 -90
  2. package/dist/attachment-magic.d.ts +13 -0
  3. package/dist/attachment-magic.d.ts.map +1 -1
  4. package/dist/attachment-magic.js +47 -0
  5. package/dist/attachment-magic.js.map +1 -1
  6. package/dist/attachment-registry.d.ts.map +1 -1
  7. package/dist/attachment-registry.js +13 -2
  8. package/dist/attachment-registry.js.map +1 -1
  9. package/dist/config/buffer-limits.d.ts +1 -2
  10. package/dist/config/buffer-limits.d.ts.map +1 -1
  11. package/dist/config/buffer-limits.js +1 -2
  12. package/dist/config/buffer-limits.js.map +1 -1
  13. package/dist/config/map-limits.d.ts +11 -0
  14. package/dist/config/map-limits.d.ts.map +1 -1
  15. package/dist/config/map-limits.js +11 -0
  16. package/dist/config/map-limits.js.map +1 -1
  17. package/dist/config/server-timing.d.ts +6 -0
  18. package/dist/config/server-timing.d.ts.map +1 -1
  19. package/dist/config/server-timing.js +9 -0
  20. package/dist/config/server-timing.js.map +1 -1
  21. package/dist/config/terminal-history.d.ts +11 -6
  22. package/dist/config/terminal-history.d.ts.map +1 -1
  23. package/dist/config/terminal-history.js +18 -8
  24. package/dist/config/terminal-history.js.map +1 -1
  25. package/dist/cron/cron-input.d.ts +30 -0
  26. package/dist/cron/cron-input.d.ts.map +1 -0
  27. package/dist/cron/cron-input.js +7 -0
  28. package/dist/cron/cron-input.js.map +1 -0
  29. package/dist/cron/cron-service.d.ts +92 -0
  30. package/dist/cron/cron-service.d.ts.map +1 -0
  31. package/dist/cron/cron-service.js +606 -0
  32. package/dist/cron/cron-service.js.map +1 -0
  33. package/dist/cron/cron-time.d.ts +29 -0
  34. package/dist/cron/cron-time.d.ts.map +1 -0
  35. package/dist/cron/cron-time.js +87 -0
  36. package/dist/cron/cron-time.js.map +1 -0
  37. package/dist/document-thumbnailer.d.ts +1 -1
  38. package/dist/document-thumbnailer.d.ts.map +1 -1
  39. package/dist/document-thumbnailer.js +11 -2
  40. package/dist/document-thumbnailer.js.map +1 -1
  41. package/dist/generated-artifact-attachments.d.ts +26 -0
  42. package/dist/generated-artifact-attachments.d.ts.map +1 -0
  43. package/dist/generated-artifact-attachments.js +61 -0
  44. package/dist/generated-artifact-attachments.js.map +1 -0
  45. package/dist/index.js +11 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/mux-interface.d.ts +31 -5
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/remote-hosts.d.ts +66 -0
  50. package/dist/remote-hosts.d.ts.map +1 -0
  51. package/dist/remote-hosts.js +198 -0
  52. package/dist/remote-hosts.js.map +1 -0
  53. package/dist/services/unified-session-service.d.ts +115 -0
  54. package/dist/services/unified-session-service.d.ts.map +1 -0
  55. package/dist/services/unified-session-service.js +173 -0
  56. package/dist/services/unified-session-service.js.map +1 -0
  57. package/dist/session-cli-builder.d.ts +3 -1
  58. package/dist/session-cli-builder.d.ts.map +1 -1
  59. package/dist/session-cli-builder.js +26 -7
  60. package/dist/session-cli-builder.js.map +1 -1
  61. package/dist/session-pty-exit-breaker.d.ts +66 -0
  62. package/dist/session-pty-exit-breaker.d.ts.map +1 -0
  63. package/dist/session-pty-exit-breaker.js +77 -0
  64. package/dist/session-pty-exit-breaker.js.map +1 -0
  65. package/dist/session.d.ts +24 -1
  66. package/dist/session.d.ts.map +1 -1
  67. package/dist/session.js +94 -11
  68. package/dist/session.js.map +1 -1
  69. package/dist/state-store.d.ts +14 -0
  70. package/dist/state-store.d.ts.map +1 -1
  71. package/dist/state-store.js +49 -0
  72. package/dist/state-store.js.map +1 -1
  73. package/dist/tmux-manager.d.ts +94 -8
  74. package/dist/tmux-manager.d.ts.map +1 -1
  75. package/dist/tmux-manager.js +222 -26
  76. package/dist/tmux-manager.js.map +1 -1
  77. package/dist/types/api.d.ts +11 -0
  78. package/dist/types/api.d.ts.map +1 -1
  79. package/dist/types/api.js.map +1 -1
  80. package/dist/types/app-state.d.ts +5 -0
  81. package/dist/types/app-state.d.ts.map +1 -1
  82. package/dist/types/app-state.js.map +1 -1
  83. package/dist/types/cron.d.ts +88 -0
  84. package/dist/types/cron.d.ts.map +1 -0
  85. package/dist/types/cron.js +13 -0
  86. package/dist/types/cron.js.map +1 -0
  87. package/dist/types/session.d.ts +57 -0
  88. package/dist/types/session.d.ts.map +1 -1
  89. package/dist/types/session.js.map +1 -1
  90. package/dist/web/heic-jpeg-converter.d.ts +24 -0
  91. package/dist/web/heic-jpeg-converter.d.ts.map +1 -0
  92. package/dist/web/heic-jpeg-converter.js +76 -0
  93. package/dist/web/heic-jpeg-converter.js.map +1 -0
  94. package/dist/web/heic-jpeg-worker.d.ts +44 -0
  95. package/dist/web/heic-jpeg-worker.d.ts.map +1 -0
  96. package/dist/web/heic-jpeg-worker.js +78 -0
  97. package/dist/web/heic-jpeg-worker.js.map +1 -0
  98. package/dist/web/ports/cron-port.d.ts +9 -0
  99. package/dist/web/ports/cron-port.d.ts.map +1 -0
  100. package/dist/web/ports/cron-port.js +6 -0
  101. package/dist/web/ports/cron-port.js.map +1 -0
  102. package/dist/web/ports/index.d.ts +1 -0
  103. package/dist/web/ports/index.d.ts.map +1 -1
  104. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  105. package/dist/web/public/app.b0534652.js +38 -0
  106. package/dist/web/public/app.b0534652.js.br +0 -0
  107. package/dist/web/public/app.b0534652.js.gz +0 -0
  108. package/dist/web/public/{constants.1c779517.js → constants.5b18ed64.js} +65 -0
  109. package/dist/web/public/constants.5b18ed64.js.br +0 -0
  110. package/dist/web/public/constants.5b18ed64.js.gz +0 -0
  111. package/dist/web/public/cron-ui.js +328 -0
  112. package/dist/web/public/cron-ui.js.br +0 -0
  113. package/dist/web/public/cron-ui.js.gz +0 -0
  114. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  115. package/dist/web/public/index.html +313 -25
  116. package/dist/web/public/index.html.br +0 -0
  117. package/dist/web/public/index.html.gz +0 -0
  118. package/dist/web/public/input-cjk.63794d0b.js +1 -0
  119. package/dist/web/public/input-cjk.63794d0b.js.br +0 -0
  120. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  121. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  122. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  123. package/dist/web/public/mobile.0644e3ac.css +1 -0
  124. package/dist/web/public/mobile.0644e3ac.css.br +0 -0
  125. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  126. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  127. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  128. package/dist/web/public/{panels-ui.772e92c9.js → panels-ui.ca03853f.js} +25 -17
  129. package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
  130. package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
  131. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  132. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  133. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  134. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  135. package/dist/web/public/session-ui.b51814ef.js +49 -0
  136. package/dist/web/public/session-ui.b51814ef.js.br +0 -0
  137. package/dist/web/public/session-ui.b51814ef.js.gz +0 -0
  138. package/dist/web/public/settings-ui.6c67c125.js +67 -0
  139. package/dist/web/public/settings-ui.6c67c125.js.br +0 -0
  140. package/dist/web/public/settings-ui.6c67c125.js.gz +0 -0
  141. package/dist/web/public/styles.be7d626b.css +1 -0
  142. package/dist/web/public/styles.be7d626b.css.br +0 -0
  143. package/dist/web/public/styles.be7d626b.css.gz +0 -0
  144. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  145. package/dist/web/public/sw.js.gz +0 -0
  146. package/dist/web/public/terminal-ui.dbf141fa.js +3 -0
  147. package/dist/web/public/terminal-ui.dbf141fa.js.br +0 -0
  148. package/dist/web/public/terminal-ui.dbf141fa.js.gz +0 -0
  149. package/dist/web/public/ultracode-panel.js.gz +0 -0
  150. package/dist/web/public/ultracode-windows.js.gz +0 -0
  151. package/dist/web/public/upload.html.gz +0 -0
  152. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  153. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  154. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  155. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  156. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  157. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  158. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  159. package/dist/web/public/vendor/xterm.css.gz +0 -0
  160. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  161. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  162. package/dist/web/routes/case-routes.d.ts.map +1 -1
  163. package/dist/web/routes/case-routes.js +115 -1
  164. package/dist/web/routes/case-routes.js.map +1 -1
  165. package/dist/web/routes/cron-routes.d.ts +11 -0
  166. package/dist/web/routes/cron-routes.d.ts.map +1 -0
  167. package/dist/web/routes/cron-routes.js +70 -0
  168. package/dist/web/routes/cron-routes.js.map +1 -0
  169. package/dist/web/routes/index.d.ts +1 -0
  170. package/dist/web/routes/index.d.ts.map +1 -1
  171. package/dist/web/routes/index.js +1 -0
  172. package/dist/web/routes/index.js.map +1 -1
  173. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  174. package/dist/web/routes/respawn-routes.js +3 -0
  175. package/dist/web/routes/respawn-routes.js.map +1 -1
  176. package/dist/web/routes/session-routes.d.ts.map +1 -1
  177. package/dist/web/routes/session-routes.js +610 -68
  178. package/dist/web/routes/session-routes.js.map +1 -1
  179. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  180. package/dist/web/routes/ws-routes.js +56 -17
  181. package/dist/web/routes/ws-routes.js.map +1 -1
  182. package/dist/web/schemas.d.ts +113 -0
  183. package/dist/web/schemas.d.ts.map +1 -1
  184. package/dist/web/schemas.js +145 -0
  185. package/dist/web/schemas.js.map +1 -1
  186. package/dist/web/server.d.ts +8 -2
  187. package/dist/web/server.d.ts.map +1 -1
  188. package/dist/web/server.js +75 -13
  189. package/dist/web/server.js.map +1 -1
  190. package/dist/web/session-listener-wiring.d.ts +5 -1
  191. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  192. package/dist/web/session-listener-wiring.js +23 -1
  193. package/dist/web/session-listener-wiring.js.map +1 -1
  194. package/dist/web/sse-events.d.ts +15 -0
  195. package/dist/web/sse-events.d.ts.map +1 -1
  196. package/dist/web/sse-events.js +17 -0
  197. package/dist/web/sse-events.js.map +1 -1
  198. package/dist/web/ws-connection-registry.d.ts +77 -0
  199. package/dist/web/ws-connection-registry.d.ts.map +1 -0
  200. package/dist/web/ws-connection-registry.js +99 -0
  201. package/dist/web/ws-connection-registry.js.map +1 -0
  202. package/package.json +3 -1
  203. package/dist/web/public/app.a8d73c32.js +0 -38
  204. package/dist/web/public/app.a8d73c32.js.br +0 -0
  205. package/dist/web/public/app.a8d73c32.js.gz +0 -0
  206. package/dist/web/public/constants.1c779517.js.br +0 -0
  207. package/dist/web/public/constants.1c779517.js.gz +0 -0
  208. package/dist/web/public/input-cjk.4e950e0a.js +0 -1
  209. package/dist/web/public/input-cjk.4e950e0a.js.br +0 -0
  210. package/dist/web/public/input-cjk.4e950e0a.js.gz +0 -0
  211. package/dist/web/public/mobile.8a9aff7a.css +0 -1
  212. package/dist/web/public/mobile.8a9aff7a.css.br +0 -0
  213. package/dist/web/public/mobile.8a9aff7a.css.gz +0 -0
  214. package/dist/web/public/panels-ui.772e92c9.js.br +0 -0
  215. package/dist/web/public/panels-ui.772e92c9.js.gz +0 -0
  216. package/dist/web/public/session-ui.da3e53f2.js +0 -36
  217. package/dist/web/public/session-ui.da3e53f2.js.br +0 -0
  218. package/dist/web/public/session-ui.da3e53f2.js.gz +0 -0
  219. package/dist/web/public/settings-ui.38feb956.js +0 -61
  220. package/dist/web/public/settings-ui.38feb956.js.br +0 -0
  221. package/dist/web/public/settings-ui.38feb956.js.gz +0 -0
  222. package/dist/web/public/styles.1b3e6147.css +0 -1
  223. package/dist/web/public/styles.1b3e6147.css.br +0 -0
  224. package/dist/web/public/styles.1b3e6147.css.gz +0 -0
  225. package/dist/web/public/terminal-ui.865f21f6.js +0 -3
  226. package/dist/web/public/terminal-ui.865f21f6.js.br +0 -0
  227. package/dist/web/public/terminal-ui.865f21f6.js.gz +0 -0
@@ -21,8 +21,8 @@
21
21
  * @module tmux-manager
22
22
  */
23
23
  import { EventEmitter } from 'node:events';
24
- import { ProcessStats, PersistedRespawnConfig, type PaneInfo, type CodexConfig } from './types.js';
25
- import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats, CreateSessionOptions, RespawnPaneOptions } from './mux-interface.js';
24
+ import { ProcessStats, PersistedRespawnConfig, type PaneInfo, type SessionMode, type CodexConfig, type SessionRemote } from './types.js';
25
+ import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats, CreateSessionOptions, RespawnPaneOptions, PaneCaptureOptions } from './mux-interface.js';
26
26
  /** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
27
27
  export declare const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
28
28
  /**
@@ -52,6 +52,23 @@ export declare function resolveTmuxPaneTarget(muxName: string, paneTarget?: stri
52
52
  * output (lines like `%0:1`). Returns the pane id whose active flag is 1.
53
53
  */
54
54
  export declare function resolveActivePaneTarget(output: string): string | null;
55
+ /**
56
+ * Normalize scrollback line endings to `\r\n` so a fresh xterm replays each line
57
+ * at column 0 (COD-138).
58
+ *
59
+ * `capture-pane -p -e -S -` (full-history capture) joins scrollback rows with a
60
+ * BARE `\n`. The browser xterm is created with the default `convertEol: false`
61
+ * (correct for the live PTY stream, which already carries real `\r\n`), so a bare
62
+ * `\n` drops a row without returning the cursor to column 0. Replaying that raw
63
+ * buffer on a full page reload makes every line start one column further right —
64
+ * the diagonal "staircase". The visible/tab-switch path avoids this by repainting
65
+ * each row with an absolute cursor CSI (`formatPaneSnapshot`); the full-history
66
+ * path returns raw scrollback, so it must be CRLF-normalized here.
67
+ *
68
+ * `\r?\n → \r\n` is idempotent on already-CRLF input and leaves a lone `\r` (an
69
+ * intentional in-line column reset / overwrite) untouched.
70
+ */
71
+ export declare function normalizeScrollbackEol(buffer: string): string;
55
72
  export declare function formatPaneSnapshot(lines: string[], geometry: {
56
73
  cols: number;
57
74
  rows: number;
@@ -65,6 +82,66 @@ export declare function formatPaneSnapshot(lines: string[], geometry: {
65
82
  * stripping destructive terminal sequences before xterm.js sees them.
66
83
  */
67
84
  export declare function buildCodexCommand(config?: CodexConfig): string;
85
+ /**
86
+ * Deterministic, reattach-stable remote tmux session name for a Codeman session.
87
+ *
88
+ * Derived from the same stable field the LOCAL muxName uses (the first 8 chars of
89
+ * the sessionId), so reconnecting (which re-issues the exact same
90
+ * `ssh … new-session -A`) lands back in the SAME remote session. Must NOT be
91
+ * random/time-based — it has to be stable across reconnects.
92
+ *
93
+ * The `codeman-ssh-` prefix is deliberately chosen to FAIL a remote Codeman's
94
+ * `SAFE_MUX_NAME_PATTERN` (`^codeman-[a-f0-9-]+$`) — the `s`/`h` letters mean a
95
+ * remote instance's discovery never treats this as one of its own sessions (belt
96
+ * to the dedicated-socket suspenders above).
97
+ */
98
+ export declare function remoteTmuxSessionName(sessionId: string): string;
99
+ /**
100
+ * COD-104 — build the SSH command that launches (or reattaches) a remote
101
+ * session INSIDE a tmux server on the remote host, so the remote agent survives
102
+ * an SSH drop.
103
+ *
104
+ * Emits:
105
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
106
+ * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
107
+ * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
108
+ * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
109
+ *
110
+ * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
111
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
112
+ * prereq tmux probe and this launch connect with identical options.
113
+ *
114
+ * - `new-session -A -s codeman-ssh-<id>` = attach-if-exists-else-create
115
+ * (idempotent), so reconnect re-runs the same command and reattaches the
116
+ * still-running agent.
117
+ * - `-L codeman-remote` = a DEDICATED socket, NOT the canonical `-L codeman` a
118
+ * remote Codeman would use, so our session never collides with / gets adopted by
119
+ * an instance running on the remote host.
120
+ * - The `set` options are scoped per-session (`set -t <name>` / server-level
121
+ * `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
122
+ * - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
123
+ * runs it), so it is shell-quoted as one unit; the `cd && exec` command is in
124
+ * turn a single tmux argument (tmux runs it via `/bin/sh -c`), so the path is
125
+ * shell-quoted inside it too. This keeps escaping correct through every layer
126
+ * even when the remote path contains spaces.
127
+ */
128
+ export declare function buildRemoteLaunchCommand(options: {
129
+ mode: SessionMode;
130
+ remote: SessionRemote;
131
+ sessionId: string;
132
+ }): string;
133
+ /**
134
+ * Build the SSH command that kills the durable remote tmux session created by
135
+ * `buildRemoteLaunchCommand`. Because that session lives on a private socket
136
+ * (`-L codeman-remote`) under a stable name, killing the LOCAL ssh wrapper alone
137
+ * would orphan the remote agent forever (invisible to Codeman, still burning plan
138
+ * quota). This is fired best-effort on session kill; the shared connection args
139
+ * carry the default `-o ConnectTimeout=10` so an unreachable host fails fast.
140
+ */
141
+ export declare function buildRemoteKillCommand(options: {
142
+ remote: SessionRemote;
143
+ sessionId: string;
144
+ }): string;
68
145
  /**
69
146
  * Manages tmux sessions that wrap Claude CLI or shell processes.
70
147
  *
@@ -250,20 +327,29 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
250
327
  */
251
328
  sendInputToPane(muxName: string, paneTarget: string, input: string): boolean;
252
329
  /**
253
- * Capture the current visible text and SGR styles of a specific pane.
330
+ * Capture a pane's text and SGR styles.
254
331
  *
255
- * `capture-pane -e` is sanitized by `formatPaneSnapshot`: SGR color/style
256
- * codes are preserved, while cursor/erase/scroll-region controls are stripped
257
- * before rows are repainted at absolute positions in browser xterm.
332
+ * Two modes:
333
+ * - Visible (default): `capture-pane -p -e` grabs only the on-screen frame,
334
+ * then `formatPaneSnapshot` repaints each row at its absolute position so
335
+ * the browser xterm reproduces the live frame. Used for fast tab switches.
336
+ * - Full history (`opts.fullHistory`): `capture-pane -p -e -J -S -<N>` grabs
337
+ * the tmux scrollback (COD-47, bounded to the configured history limit),
338
+ * returned as linear scrollback text with SGR codes preserved (NOT
339
+ * repositioned — a multi-screen history can't be painted into a single
340
+ * visible frame, so the snapshot repaint is skipped). `-J` re-joins lines
341
+ * hard-wrapped at the pane width so they reflow in the browser xterm.
342
+ * Used for full page reloads so the user gets back their scroll history.
343
+ * Caveat: lines tmux has already evicted past its history-limit are gone.
258
344
  */
259
- capturePaneBuffer(muxName: string, paneTarget: string): string | null;
345
+ capturePaneBuffer(muxName: string, paneTarget?: string, opts?: PaneCaptureOptions): string | null;
260
346
  /**
261
347
  * Capture the active pane for a tmux session.
262
348
  *
263
349
  * Pane ids are not stable across respawns or restores, so callers should not
264
350
  * assume the first pane remains `%0`.
265
351
  */
266
- captureActivePaneBuffer(muxName: string): string | null;
352
+ captureActivePaneBuffer(muxName: string, opts?: PaneCaptureOptions): string | null;
267
353
  /**
268
354
  * Start piping pane output to a file using tmux pipe-pane.
269
355
  * Only pipes output direction (-O) to avoid echoing input.
@@ -1 +1 @@
1
- {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
@@ -30,12 +30,20 @@ import { dirname } from 'node:path';
30
30
  import { dataPath, DEFAULT_TMUX_SOCKET } from './config/instance.js';
31
31
  import { getErrorMessage, DEFAULT_NICE_CONFIG, } from './types.js';
32
32
  import { buildEffortCliArgs } from './session-cli-builder.js';
33
+ import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteSshTarget } from './remote-hosts.js';
33
34
  import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir, resolveGeminiDir, } from './utils/index.js';
34
35
  // ============================================================================
35
36
  // Timing Constants
36
37
  // ============================================================================
37
38
  import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
38
- import { DEFAULT_TMUX_HISTORY_LIMIT } from './config/terminal-history.js';
39
+ import { DEFAULT_TMUX_HISTORY_LIMIT, DEFAULT_TERMINAL_BUFFER_MAX_BYTES } from './config/terminal-history.js';
40
+ /**
41
+ * Extra stdout headroom for the full-history `capture-pane` child process on
42
+ * top of the consumer's byte cap: raw scrollback carries per-line SGR/ANSI
43
+ * overhead that the route pipeline strips before applying its cap, so the
44
+ * capture must be allowed to exceed the final payload size.
45
+ */
46
+ const FULL_HISTORY_CAPTURE_SLACK_BYTES = 8 * 1024 * 1024;
39
47
  /** Delay after tmux session creation — enough for detached tmux to be queryable */
40
48
  const TMUX_CREATION_WAIT_MS = 100;
41
49
  /** Max retries for getPanePid — tmux server cold-start (e.g. macOS) may need extra time */
@@ -366,6 +374,25 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
366
374
  }
367
375
  return result;
368
376
  }
377
+ /**
378
+ * Normalize scrollback line endings to `\r\n` so a fresh xterm replays each line
379
+ * at column 0 (COD-138).
380
+ *
381
+ * `capture-pane -p -e -S -` (full-history capture) joins scrollback rows with a
382
+ * BARE `\n`. The browser xterm is created with the default `convertEol: false`
383
+ * (correct for the live PTY stream, which already carries real `\r\n`), so a bare
384
+ * `\n` drops a row without returning the cursor to column 0. Replaying that raw
385
+ * buffer on a full page reload makes every line start one column further right —
386
+ * the diagonal "staircase". The visible/tab-switch path avoids this by repainting
387
+ * each row with an absolute cursor CSI (`formatPaneSnapshot`); the full-history
388
+ * path returns raw scrollback, so it must be CRLF-normalized here.
389
+ *
390
+ * `\r?\n → \r\n` is idempotent on already-CRLF input and leaves a lone `\r` (an
391
+ * intentional in-line column reset / overwrite) untouched.
392
+ */
393
+ export function normalizeScrollbackEol(buffer) {
394
+ return buffer.replace(/\r?\n/g, '\r\n');
395
+ }
369
396
  export function formatPaneSnapshot(lines, geometry) {
370
397
  const cols = Math.max(1, geometry.cols);
371
398
  // Paint the full pane width. Earlier this dropped the rightmost column
@@ -572,6 +599,107 @@ function buildSpawnCommand(options) {
572
599
  }
573
600
  return '$SHELL';
574
601
  }
602
+ /**
603
+ * Dedicated socket for Codeman-launched REMOTE tmux servers, distinct from the
604
+ * canonical local `-L codeman` socket. A remote host that runs its OWN Codeman
605
+ * would otherwise share the `-L codeman` socket AND the `codeman-<hex>` discovery
606
+ * name, so its `reconcileSessions()` would ADOPT our session (attach a PTY,
607
+ * resize, respawn-pane it locally) — the cross-machine form of the "2nd instance
608
+ * attaches live sessions" hazard. A private socket keeps our remote sessions off
609
+ * that instance's radar entirely.
610
+ */
611
+ const REMOTE_TMUX_SOCKET = 'codeman-remote';
612
+ /**
613
+ * Deterministic, reattach-stable remote tmux session name for a Codeman session.
614
+ *
615
+ * Derived from the same stable field the LOCAL muxName uses (the first 8 chars of
616
+ * the sessionId), so reconnecting (which re-issues the exact same
617
+ * `ssh … new-session -A`) lands back in the SAME remote session. Must NOT be
618
+ * random/time-based — it has to be stable across reconnects.
619
+ *
620
+ * The `codeman-ssh-` prefix is deliberately chosen to FAIL a remote Codeman's
621
+ * `SAFE_MUX_NAME_PATTERN` (`^codeman-[a-f0-9-]+$`) — the `s`/`h` letters mean a
622
+ * remote instance's discovery never treats this as one of its own sessions (belt
623
+ * to the dedicated-socket suspenders above).
624
+ */
625
+ export function remoteTmuxSessionName(sessionId) {
626
+ return `codeman-ssh-${sessionId.slice(0, 8)}`;
627
+ }
628
+ /**
629
+ * COD-104 — build the SSH command that launches (or reattaches) a remote
630
+ * session INSIDE a tmux server on the remote host, so the remote agent survives
631
+ * an SSH drop.
632
+ *
633
+ * Emits:
634
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
635
+ * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
636
+ * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
637
+ * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
638
+ *
639
+ * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
640
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
641
+ * prereq tmux probe and this launch connect with identical options.
642
+ *
643
+ * - `new-session -A -s codeman-ssh-<id>` = attach-if-exists-else-create
644
+ * (idempotent), so reconnect re-runs the same command and reattaches the
645
+ * still-running agent.
646
+ * - `-L codeman-remote` = a DEDICATED socket, NOT the canonical `-L codeman` a
647
+ * remote Codeman would use, so our session never collides with / gets adopted by
648
+ * an instance running on the remote host.
649
+ * - The `set` options are scoped per-session (`set -t <name>` / server-level
650
+ * `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
651
+ * - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
652
+ * runs it), so it is shell-quoted as one unit; the `cd && exec` command is in
653
+ * turn a single tmux argument (tmux runs it via `/bin/sh -c`), so the path is
654
+ * shell-quoted inside it too. This keeps escaping correct through every layer
655
+ * even when the remote path contains spaces.
656
+ */
657
+ export function buildRemoteLaunchCommand(options) {
658
+ const { mode, remote, sessionId } = options;
659
+ const modeCommand = remote.commands?.[mode] || defaultRemoteCommandForMode(mode);
660
+ const remoteName = remoteTmuxSessionName(sessionId);
661
+ // Innermost: the command tmux runs in the new pane. Run via `/bin/sh -c` by
662
+ // tmux, so the path needs shell-quoting here. `exec` replaces the shell with
663
+ // the CLI so the pane PID is the agent itself.
664
+ const paneCommand = `cd ${shellescape(remote.remotePath)} && ${modeCommand}`;
665
+ // The tmux command line, with `\;` separating commands so the config `set`s
666
+ // apply on the SAME connection (and are idempotent on reattach). Options are
667
+ // scoped per-session (`set -t <name>` / server `set -s`), NEVER `-g`, so a
668
+ // shared remote tmux server's other sessions keep their own prefix/mouse.
669
+ const tmuxInvocation = [
670
+ `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
671
+ `set -t ${remoteName} status off`,
672
+ `set -t ${remoteName} mouse off`,
673
+ `set -t ${remoteName} prefix C-q`,
674
+ 'set -s escape-time 0',
675
+ ].join(' \\; ');
676
+ // ssh runs its trailing args through the remote login shell, so the entire
677
+ // tmux invocation is passed as one shell-quoted argument.
678
+ //
679
+ // COD-107 — connection options (port, identity, SOCKS ProxyCommand, jump host,
680
+ // arbitrary -o) come from the shared `buildSshConnectionArgs` so the launch and
681
+ // the tmux-prereq probe connect IDENTICALLY. `-t` is inserted right after
682
+ // `ssh -o BatchMode=yes` (preserving the historical token order), then the rest
683
+ // of the connection args, then the target and the quoted tmux invocation.
684
+ const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
685
+ const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
686
+ return sshParts.join(' ');
687
+ }
688
+ /**
689
+ * Build the SSH command that kills the durable remote tmux session created by
690
+ * `buildRemoteLaunchCommand`. Because that session lives on a private socket
691
+ * (`-L codeman-remote`) under a stable name, killing the LOCAL ssh wrapper alone
692
+ * would orphan the remote agent forever (invisible to Codeman, still burning plan
693
+ * quota). This is fired best-effort on session kill; the shared connection args
694
+ * carry the default `-o ConnectTimeout=10` so an unreachable host fails fast.
695
+ */
696
+ export function buildRemoteKillCommand(options) {
697
+ const { remote, sessionId } = options;
698
+ const remoteName = remoteTmuxSessionName(sessionId);
699
+ const killCmd = `tmux -L ${REMOTE_TMUX_SOCKET} kill-session -t ${shellescape(remoteName)}`;
700
+ const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
701
+ return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
702
+ }
575
703
  /**
576
704
  * Set sensitive environment variables on a tmux session via setenv.
577
705
  * These are inherited by panes but not visible in ps output or tmux history.
@@ -831,6 +959,12 @@ export class TmuxManager extends EventEmitter {
831
959
  'export LC_ALL=en_US.UTF-8',
832
960
  mode === 'codex' || mode === 'gemini' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
833
961
  ...(mode === 'codex' || mode === 'gemini' ? ['unset NO_COLOR'] : []),
962
+ // Stamp each Codex pane with a unique originator so the response-viewer
963
+ // can locate THIS pane's rollout exactly — codex writes the value into
964
+ // session_meta.originator of every rollout it creates. Without it,
965
+ // rollouts are matched by cwd+mtime and two panes in the same directory
966
+ // bleed into each other.
967
+ ...(mode === 'codex' ? [`export CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codeman_${sessionId}`] : []),
834
968
  'export CODEMAN_MUX=1',
835
969
  `export CODEMAN_SESSION_ID=${sessionId}`,
836
970
  `export CODEMAN_MUX_NAME=${muxName}`,
@@ -941,7 +1075,7 @@ export class TmuxManager extends EventEmitter {
941
1075
  * In test mode: creates an in-memory session only (no real tmux session).
942
1076
  */
943
1077
  async createSession(options) {
944
- const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, } = options;
1078
+ const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
945
1079
  const muxName = `codeman-${sessionId.slice(0, 8)}`;
946
1080
  if (!isValidMuxName(muxName)) {
947
1081
  throw new Error('Invalid session name: contains unsafe characters');
@@ -957,6 +1091,7 @@ export class TmuxManager extends EventEmitter {
957
1091
  pid: 99999,
958
1092
  createdAt: Date.now(),
959
1093
  workingDir,
1094
+ remote,
960
1095
  mode,
961
1096
  attached: false,
962
1097
  name,
@@ -996,7 +1131,8 @@ export class TmuxManager extends EventEmitter {
996
1131
  const cmd = wrapWithNice(baseCmd, config);
997
1132
  try {
998
1133
  // Build the full command to run inside tmux
999
- const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1134
+ const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1135
+ const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
1000
1136
  // Create tmux session in three steps to handle cold-start (no server running)
1001
1137
  // and avoid the race where the command exits before remain-on-exit is set:
1002
1138
  // 1. Create session with default shell (starts tmux server, stays alive)
@@ -1044,7 +1180,7 @@ export class TmuxManager extends EventEmitter {
1044
1180
  this.applyEnvOverrides(muxName, envOverrides);
1045
1181
  // Replace the shell with the actual command (no echo in terminal). Keep
1046
1182
  // pane launch in /tmp, then cd inside bash against the current mount table.
1047
- const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1183
+ const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1048
1184
  execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
1049
1185
  timeout: EXEC_TIMEOUT_MS,
1050
1186
  stdio: 'ignore',
@@ -1069,7 +1205,8 @@ export class TmuxManager extends EventEmitter {
1069
1205
  /* Already set globally as fallback */
1070
1206
  }),
1071
1207
  // Raise tmux scrollback from its 2000-line default so re-attach preserves
1072
- // more context. Matches the xterm-side default in constants.js.
1208
+ // more context. Intentionally exceeds the xterm-side DEFAULT_SCROLLBACK (50k
1209
+ // in constants.js), which stays lower to protect browser/mobile memory.
1073
1210
  execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, {
1074
1211
  timeout: EXEC_TIMEOUT_MS,
1075
1212
  })
@@ -1106,6 +1243,7 @@ export class TmuxManager extends EventEmitter {
1106
1243
  pid,
1107
1244
  createdAt: Date.now(),
1108
1245
  workingDir,
1246
+ remote,
1109
1247
  mode,
1110
1248
  attached: false,
1111
1249
  name,
@@ -1173,7 +1311,7 @@ export class TmuxManager extends EventEmitter {
1173
1311
  * preserving the session and its scrollback buffer.
1174
1312
  */
1175
1313
  async respawnPane(options) {
1176
- const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, } = options;
1314
+ const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
1177
1315
  const session = this.sessions.get(sessionId);
1178
1316
  if (!session)
1179
1317
  return null;
@@ -1206,7 +1344,8 @@ export class TmuxManager extends EventEmitter {
1206
1344
  });
1207
1345
  const config = niceConfig || DEFAULT_NICE_CONFIG;
1208
1346
  const cmd = wrapWithNice(baseCmd, config);
1209
- const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1347
+ const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1348
+ const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
1210
1349
  try {
1211
1350
  // For OpenCode: set sensitive env vars via tmux setenv before respawn
1212
1351
  if (mode === 'opencode') {
@@ -1221,7 +1360,8 @@ export class TmuxManager extends EventEmitter {
1221
1360
  }
1222
1361
  // Re-apply user env overrides before respawn so the new shell inherits them.
1223
1362
  this.applyEnvOverrides(muxName, envOverrides);
1224
- const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1363
+ // -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
1364
+ const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1225
1365
  await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
1226
1366
  timeout: EXEC_TIMEOUT_MS,
1227
1367
  });
@@ -1381,6 +1521,20 @@ export class TmuxManager extends EventEmitter {
1381
1521
  // Session may already be dead
1382
1522
  }
1383
1523
  }
1524
+ // Strategy 3b: Remote sessions run a DURABLE tmux server on the remote host
1525
+ // (survives ssh drops), so killing only the local ssh wrapper above would
1526
+ // orphan the remote agent forever. Fire a best-effort `ssh … tmux kill-session`
1527
+ // — fire-and-forget so it NEVER blocks or throws the local kill (bounded by the
1528
+ // shared ConnectTimeout on an unreachable host).
1529
+ if (session.remote) {
1530
+ try {
1531
+ const remoteKillCmd = buildRemoteKillCommand({ remote: session.remote, sessionId });
1532
+ exec(remoteKillCmd, { timeout: EXEC_TIMEOUT_MS }, () => { });
1533
+ }
1534
+ catch {
1535
+ // Best-effort — a failure here must not affect the local kill result.
1536
+ }
1537
+ }
1384
1538
  // Strategy 4: Direct kill by PID as final fallback
1385
1539
  if (this.isProcessAlive(currentPid)) {
1386
1540
  try {
@@ -1951,29 +2105,60 @@ export class TmuxManager extends EventEmitter {
1951
2105
  }
1952
2106
  }
1953
2107
  /**
1954
- * Capture the current visible text and SGR styles of a specific pane.
2108
+ * Capture a pane's text and SGR styles.
1955
2109
  *
1956
- * `capture-pane -e` is sanitized by `formatPaneSnapshot`: SGR color/style
1957
- * codes are preserved, while cursor/erase/scroll-region controls are stripped
1958
- * before rows are repainted at absolute positions in browser xterm.
2110
+ * Two modes:
2111
+ * - Visible (default): `capture-pane -p -e` grabs only the on-screen frame,
2112
+ * then `formatPaneSnapshot` repaints each row at its absolute position so
2113
+ * the browser xterm reproduces the live frame. Used for fast tab switches.
2114
+ * - Full history (`opts.fullHistory`): `capture-pane -p -e -J -S -<N>` grabs
2115
+ * the tmux scrollback (COD-47, bounded to the configured history limit),
2116
+ * returned as linear scrollback text with SGR codes preserved (NOT
2117
+ * repositioned — a multi-screen history can't be painted into a single
2118
+ * visible frame, so the snapshot repaint is skipped). `-J` re-joins lines
2119
+ * hard-wrapped at the pane width so they reflow in the browser xterm.
2120
+ * Used for full page reloads so the user gets back their scroll history.
2121
+ * Caveat: lines tmux has already evicted past its history-limit are gone.
1959
2122
  */
1960
- capturePaneBuffer(muxName, paneTarget) {
2123
+ capturePaneBuffer(muxName, paneTarget, opts) {
1961
2124
  if (IS_TEST_MODE)
1962
2125
  return '';
1963
- if (!isValidMuxName(muxName)) {
1964
- console.error('[TmuxManager] Invalid session name in capturePaneBuffer:', muxName);
1965
- return null;
1966
- }
1967
- if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) {
1968
- console.error('[TmuxManager] Invalid pane target:', paneTarget);
2126
+ const target = resolveTmuxPaneTarget(muxName, paneTarget);
2127
+ if (!target) {
2128
+ console.error('[TmuxManager] Invalid pane target in capturePaneBuffer:', { muxName, paneTarget });
1969
2129
  return null;
1970
2130
  }
1971
- const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
2131
+ const fullHistory = opts?.fullHistory === true;
1972
2132
  try {
1973
- const buffer = execSync(`${this.tmux()} capture-pane -p -e -t ${shellescape(target)}`, {
2133
+ // `-S -<N>` starts the capture N lines above the visible frame (tmux
2134
+ // clamps to the top of history), so tmux never serializes more scrollback
2135
+ // than the configured history limit retains.
2136
+ const requestedLines = opts?.historyLimitLines;
2137
+ const historyLines = typeof requestedLines === 'number' && Number.isFinite(requestedLines) && requestedLines > 0
2138
+ ? Math.trunc(requestedLines)
2139
+ : DEFAULT_TMUX_HISTORY_LIMIT;
2140
+ const captureFlags = fullHistory ? `capture-pane -p -e -J -S -${historyLines}` : 'capture-pane -p -e';
2141
+ // execSync's default maxBuffer (1MB) kills multi-MB scrollback dumps
2142
+ // (ENOBUFS) and would silently degrade full-history capture to the byte
2143
+ // buffer for exactly the long sessions it exists for — size it from the
2144
+ // consumer's byte cap plus ANSI-overhead slack instead.
2145
+ const execOpts = {
1974
2146
  encoding: 'utf-8',
1975
2147
  timeout: EXEC_TIMEOUT_MS,
1976
- }).replace(/\n+$/g, '');
2148
+ };
2149
+ if (fullHistory) {
2150
+ execOpts.maxBuffer =
2151
+ (opts?.maxCaptureBytes ?? DEFAULT_TERMINAL_BUFFER_MAX_BYTES) + FULL_HISTORY_CAPTURE_SLACK_BYTES;
2152
+ }
2153
+ const buffer = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts).replace(/\n+$/g, '');
2154
+ // Full-history spans many screens — return it as raw linear scrollback
2155
+ // rather than repainting rows at single-screen absolute positions. tmux
2156
+ // joins scrollback rows with a bare `\n`; normalize to `\r\n` so a fresh
2157
+ // xterm (convertEol:false) starts each replayed line at column 0 instead
2158
+ // of staircasing diagonally (COD-138).
2159
+ if (fullHistory) {
2160
+ return normalizeScrollbackEol(buffer);
2161
+ }
1977
2162
  try {
1978
2163
  const cursor = execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, {
1979
2164
  encoding: 'utf-8',
@@ -1994,10 +2179,21 @@ export class TmuxManager extends EventEmitter {
1994
2179
  catch (cursorErr) {
1995
2180
  console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
1996
2181
  }
1997
- return buffer;
2182
+ // Cursor query failed or geometry was invalid, so we skip the absolute-
2183
+ // positioned snapshot repaint and fall back to the raw capture. Normalize
2184
+ // its bare `\n` line endings to `\r\n` so the replay doesn't staircase
2185
+ // diagonally in a fresh xterm (COD-138, same reason as the fullHistory path).
2186
+ return normalizeScrollbackEol(buffer);
1998
2187
  }
1999
2188
  catch (err) {
2000
- console.error('[TmuxManager] Failed to capture pane buffer:', err);
2189
+ // ENOBUFS carries the truncated multi-MB stdout on the error object —
2190
+ // log a concise line instead of dumping it into the journal.
2191
+ if (err?.code === 'ENOBUFS') {
2192
+ console.error('[TmuxManager] Pane capture exceeded maxBuffer (ENOBUFS); falling back to byte history');
2193
+ }
2194
+ else {
2195
+ console.error('[TmuxManager] Failed to capture pane buffer:', err);
2196
+ }
2001
2197
  return null;
2002
2198
  }
2003
2199
  }
@@ -2007,7 +2203,7 @@ export class TmuxManager extends EventEmitter {
2007
2203
  * Pane ids are not stable across respawns or restores, so callers should not
2008
2204
  * assume the first pane remains `%0`.
2009
2205
  */
2010
- captureActivePaneBuffer(muxName) {
2206
+ captureActivePaneBuffer(muxName, opts) {
2011
2207
  if (IS_TEST_MODE)
2012
2208
  return '';
2013
2209
  if (!isValidMuxName(muxName)) {
@@ -2020,7 +2216,7 @@ export class TmuxManager extends EventEmitter {
2020
2216
  timeout: EXEC_TIMEOUT_MS,
2021
2217
  }).trim();
2022
2218
  const target = resolveActivePaneTarget(output);
2023
- return target ? this.capturePaneBuffer(muxName, target) : null;
2219
+ return target ? this.capturePaneBuffer(muxName, target, opts) : null;
2024
2220
  }
2025
2221
  catch (err) {
2026
2222
  console.error('[TmuxManager] Failed to resolve active pane for capture:', err);