aicodeman 0.5.2 → 0.5.4

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 (144) hide show
  1. package/dist/ai-checker-base.d.ts.map +1 -1
  2. package/dist/ai-checker-base.js +3 -2
  3. package/dist/ai-checker-base.js.map +1 -1
  4. package/dist/bash-tool-parser.d.ts +6 -0
  5. package/dist/bash-tool-parser.d.ts.map +1 -1
  6. package/dist/bash-tool-parser.js +87 -101
  7. package/dist/bash-tool-parser.js.map +1 -1
  8. package/dist/file-stream-manager.d.ts.map +1 -1
  9. package/dist/file-stream-manager.js +2 -1
  10. package/dist/file-stream-manager.js.map +1 -1
  11. package/dist/orchestrator-loop.d.ts +2 -0
  12. package/dist/orchestrator-loop.d.ts.map +1 -1
  13. package/dist/orchestrator-loop.js +27 -22
  14. package/dist/orchestrator-loop.js.map +1 -1
  15. package/dist/orchestrator-verifier.d.ts +1 -1
  16. package/dist/orchestrator-verifier.d.ts.map +1 -1
  17. package/dist/orchestrator-verifier.js +3 -2
  18. package/dist/orchestrator-verifier.js.map +1 -1
  19. package/dist/plan-orchestrator.d.ts +4 -1
  20. package/dist/plan-orchestrator.d.ts.map +1 -1
  21. package/dist/plan-orchestrator.js +66 -88
  22. package/dist/plan-orchestrator.js.map +1 -1
  23. package/dist/ralph-status-parser.d.ts +2 -0
  24. package/dist/ralph-status-parser.d.ts.map +1 -1
  25. package/dist/ralph-status-parser.js +98 -102
  26. package/dist/ralph-status-parser.js.map +1 -1
  27. package/dist/ralph-tracker.d.ts +9 -0
  28. package/dist/ralph-tracker.d.ts.map +1 -1
  29. package/dist/ralph-tracker.js +52 -60
  30. package/dist/ralph-tracker.js.map +1 -1
  31. package/dist/respawn-controller.d.ts +18 -1
  32. package/dist/respawn-controller.d.ts.map +1 -1
  33. package/dist/respawn-controller.js +215 -181
  34. package/dist/respawn-controller.js.map +1 -1
  35. package/dist/session-auto-ops.d.ts.map +1 -1
  36. package/dist/session-auto-ops.js +57 -55
  37. package/dist/session-auto-ops.js.map +1 -1
  38. package/dist/session.d.ts +5 -0
  39. package/dist/session.d.ts.map +1 -1
  40. package/dist/session.js +182 -218
  41. package/dist/session.js.map +1 -1
  42. package/dist/state-store.d.ts +6 -0
  43. package/dist/state-store.d.ts.map +1 -1
  44. package/dist/state-store.js +67 -79
  45. package/dist/state-store.js.map +1 -1
  46. package/dist/subagent-watcher.d.ts +24 -0
  47. package/dist/subagent-watcher.d.ts.map +1 -1
  48. package/dist/subagent-watcher.js +215 -220
  49. package/dist/subagent-watcher.js.map +1 -1
  50. package/dist/tmux-manager.d.ts +17 -0
  51. package/dist/tmux-manager.d.ts.map +1 -1
  52. package/dist/tmux-manager.js +57 -66
  53. package/dist/tmux-manager.js.map +1 -1
  54. package/dist/tunnel-manager.d.ts.map +1 -1
  55. package/dist/tunnel-manager.js +2 -1
  56. package/dist/tunnel-manager.js.map +1 -1
  57. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  58. package/dist/web/public/app.16290ae3.js +26 -0
  59. package/dist/web/public/app.16290ae3.js.br +0 -0
  60. package/dist/web/public/app.16290ae3.js.gz +0 -0
  61. package/dist/web/public/constants.64161167.js.gz +0 -0
  62. package/dist/web/public/index.html +7 -7
  63. package/dist/web/public/index.html.br +0 -0
  64. package/dist/web/public/index.html.gz +0 -0
  65. package/dist/web/public/input-cjk.88082175.js.gz +0 -0
  66. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  67. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  68. package/dist/web/public/mobile.0b213796.css.gz +0 -0
  69. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  70. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  71. package/dist/web/public/{panels-ui.8204db1e.js → panels-ui.2d5b9703.js} +1 -1
  72. package/dist/web/public/panels-ui.2d5b9703.js.br +0 -0
  73. package/dist/web/public/panels-ui.2d5b9703.js.gz +0 -0
  74. package/dist/web/public/{ralph-panel.a2733fd5.js → ralph-panel.61076370.js} +1 -1
  75. package/dist/web/public/ralph-panel.61076370.js.br +0 -0
  76. package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
  77. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  78. package/dist/web/public/{respawn-ui.372c6ea7.js → respawn-ui.60be6ef5.js} +1 -1
  79. package/dist/web/public/respawn-ui.60be6ef5.js.br +0 -0
  80. package/dist/web/public/respawn-ui.60be6ef5.js.gz +0 -0
  81. package/dist/web/public/{session-ui.72f2f538.js → session-ui.554092ae.js} +1 -1
  82. package/dist/web/public/session-ui.554092ae.js.br +0 -0
  83. package/dist/web/public/session-ui.554092ae.js.gz +0 -0
  84. package/dist/web/public/{settings-ui.bd3eaadb.js → settings-ui.c58b0b9b.js} +7 -7
  85. package/dist/web/public/settings-ui.c58b0b9b.js.br +0 -0
  86. package/dist/web/public/settings-ui.c58b0b9b.js.gz +0 -0
  87. package/dist/web/public/styles.111ff326.css.gz +0 -0
  88. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  89. package/dist/web/public/sw.js.gz +0 -0
  90. package/dist/web/public/terminal-ui.474f79df.js.gz +0 -0
  91. package/dist/web/public/upload.html.gz +0 -0
  92. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  93. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  94. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  95. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  96. package/dist/web/public/vendor/xterm.css.gz +0 -0
  97. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  98. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  99. package/dist/web/respawn-event-wiring.d.ts +51 -0
  100. package/dist/web/respawn-event-wiring.d.ts.map +1 -0
  101. package/dist/web/respawn-event-wiring.js +280 -0
  102. package/dist/web/respawn-event-wiring.js.map +1 -0
  103. package/dist/web/route-helpers.d.ts +23 -0
  104. package/dist/web/route-helpers.d.ts.map +1 -1
  105. package/dist/web/route-helpers.js +53 -0
  106. package/dist/web/route-helpers.js.map +1 -1
  107. package/dist/web/routes/case-routes.d.ts.map +1 -1
  108. package/dist/web/routes/case-routes.js +2 -11
  109. package/dist/web/routes/case-routes.js.map +1 -1
  110. package/dist/web/routes/file-routes.d.ts.map +1 -1
  111. package/dist/web/routes/file-routes.js +8 -24
  112. package/dist/web/routes/file-routes.js.map +1 -1
  113. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  114. package/dist/web/routes/orchestrator-routes.js +23 -30
  115. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  116. package/dist/web/routes/system-routes.d.ts.map +1 -1
  117. package/dist/web/routes/system-routes.js +17 -71
  118. package/dist/web/routes/system-routes.js.map +1 -1
  119. package/dist/web/server.d.ts +4 -51
  120. package/dist/web/server.d.ts.map +1 -1
  121. package/dist/web/server.js +98 -941
  122. package/dist/web/server.js.map +1 -1
  123. package/dist/web/session-listener-wiring.d.ts +89 -0
  124. package/dist/web/session-listener-wiring.d.ts.map +1 -0
  125. package/dist/web/session-listener-wiring.js +290 -0
  126. package/dist/web/session-listener-wiring.js.map +1 -0
  127. package/dist/web/sse-stream-manager.d.ts +91 -0
  128. package/dist/web/sse-stream-manager.d.ts.map +1 -0
  129. package/dist/web/sse-stream-manager.js +426 -0
  130. package/dist/web/sse-stream-manager.js.map +1 -0
  131. package/package.json +1 -1
  132. package/dist/web/public/app.e09fd4a6.js +0 -26
  133. package/dist/web/public/app.e09fd4a6.js.br +0 -0
  134. package/dist/web/public/app.e09fd4a6.js.gz +0 -0
  135. package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
  136. package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
  137. package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
  138. package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
  139. package/dist/web/public/respawn-ui.372c6ea7.js.br +0 -0
  140. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  141. package/dist/web/public/session-ui.72f2f538.js.br +0 -0
  142. package/dist/web/public/session-ui.72f2f538.js.gz +0 -0
  143. package/dist/web/public/settings-ui.bd3eaadb.js.br +0 -0
  144. package/dist/web/public/settings-ui.bd3eaadb.js.gz +0 -0
package/dist/session.js CHANGED
@@ -30,7 +30,7 @@
30
30
  import { EventEmitter } from 'node:events';
31
31
  import { v4 as uuidv4 } from 'uuid';
32
32
  import * as pty from 'node-pty';
33
- import { DEFAULT_NICE_CONFIG, } from './types.js';
33
+ import { DEFAULT_NICE_CONFIG, getErrorMessage, } from './types.js';
34
34
  import { TaskTracker } from './task-tracker.js';
35
35
  import { RalphTracker } from './ralph-tracker.js';
36
36
  import { BashToolParser } from './bash-tool-parser.js';
@@ -595,33 +595,75 @@ export class Session extends EventEmitter {
595
595
  * session.write('help me with this code\r');
596
596
  * ```
597
597
  */
598
+ async _setupOrAttachMuxSession(options) {
599
+ const mux = this._mux;
600
+ // Verify stale mux session — tmux may have been destroyed (e.g., killed externally)
601
+ if (this._muxSession && !mux.muxSessionExists(this._muxSession.muxName)) {
602
+ console.log('[Session] Stale mux session detected (tmux gone):', this._muxSession.muxName);
603
+ this._muxSession = null;
604
+ }
605
+ // Check if session exists but pane is dead (remain-on-exit keeps it alive)
606
+ // Respawn the pane instead of creating a whole new session — preserves tmux scrollback
607
+ let needsNewSession = false;
608
+ if (this._muxSession && mux.isPaneDead(this._muxSession.muxName)) {
609
+ console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
610
+ const newPid = await mux.respawnPane(options.respawnPaneOptions);
611
+ if (!newPid) {
612
+ console.error('[Session] Failed to respawn pane, will create new session');
613
+ needsNewSession = true;
614
+ }
615
+ else {
616
+ // Wait a moment for the respawned process to fully start
617
+ await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
618
+ }
619
+ }
620
+ // Check if we already have a mux session (restored session)
621
+ const isRestored = this._muxSession !== null && !needsNewSession;
622
+ if (isRestored) {
623
+ console.log('[Session] Attaching to existing mux session:', this._muxSession.muxName);
624
+ }
625
+ else {
626
+ // Create a new mux session
627
+ this._muxSession = await mux.createSession(options.createSessionOptions);
628
+ console.log('[Session] Created mux session:', this._muxSession.muxName);
629
+ // No extra sleep — createSession() already waits for tmux readiness
630
+ }
631
+ // Attach to the mux session via PTY
632
+ try {
633
+ this.ptyProcess = pty.spawn(mux.getAttachCommand(), mux.getAttachArgs(this._muxSession.muxName), {
634
+ name: 'xterm-256color',
635
+ cols: 120,
636
+ rows: 40,
637
+ cwd: this.workingDir,
638
+ env: buildMuxAttachEnv(),
639
+ });
640
+ }
641
+ catch (spawnErr) {
642
+ console.error(`[Session] Failed to spawn PTY for ${options.spawnErrLabel}:`, spawnErr);
643
+ this.emit('error', `Failed to attach to mux session: ${spawnErr}`);
644
+ throw spawnErr;
645
+ }
646
+ return { isRestored };
647
+ }
648
+ _handleTerminalOutput(data) {
649
+ // BufferAccumulator handles auto-trimming when max size exceeded
650
+ this._terminalBuffer.append(data);
651
+ this._lastActivityAt = Date.now();
652
+ this.emit('terminal', data);
653
+ this.emit('output', data);
654
+ }
598
655
  async startInteractive() {
599
656
  if (this.ptyProcess) {
600
657
  throw new Error('Session already has a running process');
601
658
  }
602
- this._status = 'busy';
603
- this._terminalBuffer.clear();
604
- this._textOutput.clear();
605
- this._errorBuffer = '';
606
- this._messages = [];
607
- this._lineBuffer = '';
608
- this._lastActivityAt = Date.now();
659
+ this._resetBuffers();
609
660
  const modeLabel = this.mode === 'opencode' ? 'OpenCode' : 'Claude';
610
661
  console.log(`[Session] Starting interactive ${modeLabel} session` + (this._useMux ? ` (with ${this._mux.backend})` : ''));
611
662
  // If mux wrapping is enabled, create or attach to a mux session
612
663
  if (this._useMux && this._mux) {
613
664
  try {
614
- // Verify stale mux session tmux may have been destroyed (e.g., killed externally)
615
- if (this._muxSession && !this._mux.muxSessionExists(this._muxSession.muxName)) {
616
- console.log('[Session] Stale mux session detected (tmux gone):', this._muxSession.muxName);
617
- this._muxSession = null;
618
- }
619
- // Check if session exists but pane is dead (remain-on-exit keeps it alive)
620
- // Respawn the pane instead of creating a whole new session — preserves tmux scrollback
621
- let needsNewSession = false;
622
- if (this._muxSession && this._mux.isPaneDead(this._muxSession.muxName)) {
623
- console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
624
- const newPid = await this._mux.respawnPane({
665
+ const { isRestored } = await this._setupOrAttachMuxSession({
666
+ respawnPaneOptions: {
625
667
  sessionId: this.id,
626
668
  workingDir: this.workingDir,
627
669
  mode: this.mode,
@@ -631,24 +673,8 @@ export class Session extends EventEmitter {
631
673
  allowedTools: this._allowedTools,
632
674
  openCodeConfig: this._openCodeConfig,
633
675
  resumeSessionId: this._resumeSessionId,
634
- });
635
- if (!newPid) {
636
- console.error('[Session] Failed to respawn pane, will create new session');
637
- needsNewSession = true;
638
- }
639
- else {
640
- // Wait a moment for the respawned process to fully start
641
- await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
642
- }
643
- }
644
- // Check if we already have a mux session (restored session)
645
- const isRestoredSession = this._muxSession !== null && !needsNewSession;
646
- if (isRestoredSession) {
647
- console.log('[Session] Attaching to existing mux session:', this._muxSession.muxName);
648
- }
649
- else {
650
- // Create a new mux session
651
- this._muxSession = await this._mux.createSession({
676
+ },
677
+ createSessionOptions: {
652
678
  sessionId: this.id,
653
679
  workingDir: this.workingDir,
654
680
  mode: this.mode,
@@ -659,30 +685,14 @@ export class Session extends EventEmitter {
659
685
  allowedTools: this._allowedTools,
660
686
  openCodeConfig: this._openCodeConfig,
661
687
  resumeSessionId: this._resumeSessionId,
662
- });
663
- console.log('[Session] Created mux session:', this._muxSession.muxName);
664
- // No extra sleep — createSession() already waits for tmux readiness
665
- }
666
- // Attach to the mux session via PTY
667
- try {
668
- this.ptyProcess = pty.spawn(this._mux.getAttachCommand(), this._mux.getAttachArgs(this._muxSession.muxName), {
669
- name: 'xterm-256color',
670
- cols: 120,
671
- rows: 40,
672
- cwd: this.workingDir,
673
- env: buildMuxAttachEnv(),
674
- });
675
- // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
676
- this._claudeSessionId = this._resumeSessionId || this.id;
677
- }
678
- catch (spawnErr) {
679
- console.error('[Session] Failed to spawn PTY for mux attachment:', spawnErr);
680
- this.emit('error', `Failed to attach to mux session: ${spawnErr}`);
681
- throw spawnErr;
682
- }
688
+ },
689
+ spawnErrLabel: 'mux attachment',
690
+ });
691
+ // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
692
+ this._claudeSessionId = this._resumeSessionId || this.id;
683
693
  // For NEW mux sessions: wait for readiness then clean buffer
684
694
  // For RESTORED mux sessions: don't do anything - client will fetch buffer on tab switch
685
- if (!isRestoredSession) {
695
+ if (!isRestored) {
686
696
  if (this.mode === 'opencode') {
687
697
  // OpenCode uses Bubble Tea TUI — no ❯ prompt to detect.
688
698
  // Wait for TUI to stabilize (output stops changing), then mark ready.
@@ -769,11 +779,7 @@ export class Session extends EventEmitter {
769
779
  const data = rawData.replace(FOCUS_ESCAPE_FILTER, '').replace(CTRL_L_PATTERN, ''); // Remove Ctrl+L
770
780
  if (!data)
771
781
  return; // Skip if only filtered sequences
772
- // BufferAccumulator handles auto-trimming when max size exceeded
773
- this._terminalBuffer.append(data);
774
- this._lastActivityAt = Date.now();
775
- this.emit('terminal', data);
776
- this.emit('output', data);
782
+ this._handleTerminalOutput(data);
777
783
  // === Idle/working detection runs on every chunk (latency-sensitive) ===
778
784
  // Detect if Claude is working or at prompt
779
785
  // The prompt line contains "❯" when waiting for input
@@ -958,77 +964,32 @@ export class Session extends EventEmitter {
958
964
  if (this.ptyProcess) {
959
965
  throw new Error('Session already has a running process');
960
966
  }
961
- this._status = 'busy';
962
- this._terminalBuffer.clear();
963
- this._textOutput.clear();
964
- this._errorBuffer = '';
965
- this._messages = [];
966
- this._lineBuffer = '';
967
- this._lastActivityAt = Date.now();
967
+ this._resetBuffers();
968
968
  // Use user's default shell or bash
969
969
  const shell = process.env.SHELL || '/bin/bash';
970
970
  console.log('[Session] Starting shell session with:', shell + (this._useMux ? ` (with ${this._mux.backend})` : ''));
971
971
  // If mux wrapping is enabled, create or attach to a mux session
972
972
  if (this._useMux && this._mux) {
973
973
  try {
974
- // Verify stale mux session tmux may have been destroyed externally
975
- if (this._muxSession && !this._mux.muxSessionExists(this._muxSession.muxName)) {
976
- console.log('[Session] Stale mux session detected (tmux gone):', this._muxSession.muxName);
977
- this._muxSession = null;
978
- }
979
- // Check if session exists but pane is dead (remain-on-exit keeps it alive)
980
- let needsNewSession = false;
981
- if (this._muxSession && this._mux.isPaneDead(this._muxSession.muxName)) {
982
- console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
983
- const newPid = await this._mux.respawnPane({
974
+ const { isRestored } = await this._setupOrAttachMuxSession({
975
+ respawnPaneOptions: {
984
976
  sessionId: this.id,
985
977
  workingDir: this.workingDir,
986
978
  mode: 'shell',
987
979
  niceConfig: this._niceConfig,
988
- });
989
- if (!newPid) {
990
- console.error('[Session] Failed to respawn pane, will create new session');
991
- needsNewSession = true;
992
- }
993
- else {
994
- await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
995
- }
996
- }
997
- // Check if we already have a mux session (restored session)
998
- const isRestoredSession = this._muxSession !== null && !needsNewSession;
999
- if (isRestoredSession) {
1000
- console.log('[Session] Attaching to existing mux session:', this._muxSession.muxName);
1001
- }
1002
- else {
1003
- // Create a new mux session
1004
- this._muxSession = await this._mux.createSession({
980
+ },
981
+ createSessionOptions: {
1005
982
  sessionId: this.id,
1006
983
  workingDir: this.workingDir,
1007
984
  mode: 'shell',
1008
985
  name: this._name,
1009
986
  niceConfig: this._niceConfig,
1010
- });
1011
- console.log('[Session] Created mux session:', this._muxSession.muxName);
1012
- // No extra sleep — createSession() already waits for tmux readiness
1013
- }
1014
- // Attach to the mux session via PTY
1015
- try {
1016
- this.ptyProcess = pty.spawn(this._mux.getAttachCommand(), this._mux.getAttachArgs(this._muxSession.muxName), {
1017
- name: 'xterm-256color',
1018
- cols: 120,
1019
- rows: 40,
1020
- cwd: this.workingDir,
1021
- env: buildMuxAttachEnv(),
1022
- });
1023
- }
1024
- catch (spawnErr) {
1025
- console.error('[Session] Failed to spawn PTY for shell mux attachment:', spawnErr);
1026
- this.emit('error', `Failed to attach to mux session: ${spawnErr}`);
1027
- throw spawnErr;
1028
- }
987
+ },
988
+ spawnErrLabel: 'shell mux attachment',
989
+ });
1029
990
  // For NEW sessions: clear by sending 'clear' command to the shell
1030
991
  // For RESTORED sessions: don't clear - we want to see the existing output
1031
- if (!isRestoredSession) {
992
+ if (!isRestored) {
1032
993
  setTimeout(() => {
1033
994
  if (this.ptyProcess) {
1034
995
  this._terminalBuffer.clear();
@@ -1068,11 +1029,7 @@ export class Session extends EventEmitter {
1068
1029
  const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
1069
1030
  if (!data)
1070
1031
  return; // Skip if only focus sequences
1071
- // BufferAccumulator handles auto-trimming when max size exceeded
1072
- this._terminalBuffer.append(data);
1073
- this._lastActivityAt = Date.now();
1074
- this.emit('terminal', data);
1075
- this.emit('output', data);
1032
+ this._handleTerminalOutput(data);
1076
1033
  });
1077
1034
  this.ptyProcess.onExit(({ exitCode }) => {
1078
1035
  console.log('[Session] Shell PTY exited with code:', exitCode);
@@ -1130,13 +1087,7 @@ export class Session extends EventEmitter {
1130
1087
  reject(new Error('Session already has a running process'));
1131
1088
  return;
1132
1089
  }
1133
- this._status = 'busy';
1134
- this._terminalBuffer.clear();
1135
- this._textOutput.clear();
1136
- this._errorBuffer = '';
1137
- this._messages = [];
1138
- this._lineBuffer = '';
1139
- this._lastActivityAt = Date.now();
1090
+ this._resetBuffers();
1140
1091
  this._promptResolved = false; // Reset race condition guard
1141
1092
  this.resolvePromise = resolve;
1142
1093
  this.rejectPromise = reject;
@@ -1167,11 +1118,7 @@ export class Session extends EventEmitter {
1167
1118
  const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
1168
1119
  if (!data)
1169
1120
  return; // Skip if only focus sequences
1170
- // BufferAccumulator handles auto-trimming when max size exceeded
1171
- this._terminalBuffer.append(data);
1172
- this._lastActivityAt = Date.now();
1173
- this.emit('terminal', data);
1174
- this.emit('output', data);
1121
+ this._handleTerminalOutput(data);
1175
1122
  // Also try to parse JSON lines for structured data
1176
1123
  this.processOutput(data);
1177
1124
  });
@@ -1197,9 +1144,11 @@ export class Session extends EventEmitter {
1197
1144
  this._status = 'idle';
1198
1145
  const cost = resultMsg.total_cost_usd || 0;
1199
1146
  this._totalCost += cost;
1200
- this.emit('completion', resultMsg.result || '', cost);
1147
+ // Claude CLI stream-json may return empty result field fall back to accumulated text output
1148
+ const result = resultMsg.result || this._textOutput.value || '';
1149
+ this.emit('completion', result, cost);
1201
1150
  if (resolve) {
1202
- resolve({ result: resultMsg.result || '', cost });
1151
+ resolve({ result, cost });
1203
1152
  }
1204
1153
  }
1205
1154
  else if (exitCode !== 0 || (resultMsg && resultMsg.is_error)) {
@@ -1229,6 +1178,99 @@ export class Session extends EventEmitter {
1229
1178
  }
1230
1179
  });
1231
1180
  }
1181
+ _resetBuffers() {
1182
+ this._status = 'busy';
1183
+ this._terminalBuffer.clear();
1184
+ this._textOutput.clear();
1185
+ this._errorBuffer = '';
1186
+ this._messages = [];
1187
+ this._lineBuffer = '';
1188
+ this._lastActivityAt = Date.now();
1189
+ }
1190
+ _clearAllTimers() {
1191
+ // Clear activity timeout to prevent memory leak
1192
+ if (this.activityTimeout) {
1193
+ clearTimeout(this.activityTimeout);
1194
+ this.activityTimeout = null;
1195
+ }
1196
+ // Clear line buffer flush timer
1197
+ if (this._lineBufferFlushTimer) {
1198
+ clearTimeout(this._lineBufferFlushTimer);
1199
+ this._lineBufferFlushTimer = null;
1200
+ }
1201
+ // Destroy auto-compact/auto-clear automation (clears its timers)
1202
+ this._autoOps.destroy();
1203
+ // Clear prompt check timers
1204
+ if (this._promptCheckInterval) {
1205
+ clearInterval(this._promptCheckInterval);
1206
+ this._promptCheckInterval = null;
1207
+ }
1208
+ if (this._promptCheckTimeout) {
1209
+ clearTimeout(this._promptCheckTimeout);
1210
+ this._promptCheckTimeout = null;
1211
+ }
1212
+ // Clear shell idle timer
1213
+ if (this._shellIdleTimer) {
1214
+ clearTimeout(this._shellIdleTimer);
1215
+ this._shellIdleTimer = null;
1216
+ }
1217
+ // Clear expensive processing timer
1218
+ if (this._expensiveProcessTimer) {
1219
+ clearTimeout(this._expensiveProcessTimer);
1220
+ this._expensiveProcessTimer = null;
1221
+ }
1222
+ this._pendingCleanData = '';
1223
+ }
1224
+ _handleJsonMessage(cleanLine, rawLine) {
1225
+ try {
1226
+ const msg = JSON.parse(cleanLine);
1227
+ this._messages.push(msg);
1228
+ this.emit('message', msg);
1229
+ // Trim messages array for long-running sessions
1230
+ if (this._messages.length > MAX_MESSAGES) {
1231
+ this._messages = this._messages.slice(-Math.floor(MAX_MESSAGES * 0.8));
1232
+ }
1233
+ // Extract Claude session ID from messages (can be in any message type)
1234
+ // Support both sessionId (camelCase) and session_id (snake_case)
1235
+ const msgSessionId = msg.sessionId ?? msg.session_id;
1236
+ if (msgSessionId && !this._claudeSessionId) {
1237
+ this._claudeSessionId = msgSessionId;
1238
+ }
1239
+ // Process message for task tracking
1240
+ this._taskTracker.processMessage(msg);
1241
+ if (msg.type === 'assistant' && msg.message?.content) {
1242
+ for (const block of msg.message.content) {
1243
+ if (block.type === 'text' && block.text) {
1244
+ this._textOutput.append(block.text);
1245
+ }
1246
+ }
1247
+ // Track tokens from usage (with validation)
1248
+ if (msg.message.usage) {
1249
+ const inputDelta = msg.message.usage.input_tokens || 0;
1250
+ const outputDelta = msg.message.usage.output_tokens || 0;
1251
+ // Sanity check: max 100k tokens per message (generous limit)
1252
+ const MAX_TOKENS_PER_MESSAGE = 100_000;
1253
+ if (inputDelta > 0 && inputDelta <= MAX_TOKENS_PER_MESSAGE) {
1254
+ this._totalInputTokens += inputDelta;
1255
+ }
1256
+ if (outputDelta > 0 && outputDelta <= MAX_TOKENS_PER_MESSAGE) {
1257
+ this._totalOutputTokens += outputDelta;
1258
+ }
1259
+ // Check if we should auto-compact or auto-clear
1260
+ this._autoOps.checkAutoCompact();
1261
+ this._autoOps.checkAutoClear();
1262
+ }
1263
+ }
1264
+ if (msg.type === 'result' && msg.total_cost_usd) {
1265
+ this._totalCost = msg.total_cost_usd;
1266
+ }
1267
+ }
1268
+ catch (parseErr) {
1269
+ // Not JSON, just regular output - this is expected for non-JSON lines
1270
+ console.debug('[Session] Line not JSON (expected for text output):', parseErr instanceof Error ? parseErr.message : parseErr);
1271
+ this._textOutput.append(rawLine + '\n');
1272
+ }
1273
+ }
1232
1274
  processOutput(data) {
1233
1275
  // Early return if session is stopped to prevent any processing or timer creation
1234
1276
  if (this._isStopped)
@@ -1264,54 +1306,7 @@ export class Session extends EventEmitter {
1264
1306
  // Remove ANSI escape codes for JSON parsing (use pre-compiled pattern)
1265
1307
  const cleanLine = trimmed.replace(ANSI_ESCAPE_PATTERN_FULL, '');
1266
1308
  if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {
1267
- try {
1268
- const msg = JSON.parse(cleanLine);
1269
- this._messages.push(msg);
1270
- this.emit('message', msg);
1271
- // Trim messages array for long-running sessions
1272
- if (this._messages.length > MAX_MESSAGES) {
1273
- this._messages = this._messages.slice(-Math.floor(MAX_MESSAGES * 0.8));
1274
- }
1275
- // Extract Claude session ID from messages (can be in any message type)
1276
- // Support both sessionId (camelCase) and session_id (snake_case)
1277
- const msgSessionId = msg.sessionId ?? msg.session_id;
1278
- if (msgSessionId && !this._claudeSessionId) {
1279
- this._claudeSessionId = msgSessionId;
1280
- }
1281
- // Process message for task tracking
1282
- this._taskTracker.processMessage(msg);
1283
- if (msg.type === 'assistant' && msg.message?.content) {
1284
- for (const block of msg.message.content) {
1285
- if (block.type === 'text' && block.text) {
1286
- this._textOutput.append(block.text);
1287
- }
1288
- }
1289
- // Track tokens from usage (with validation)
1290
- if (msg.message.usage) {
1291
- const inputDelta = msg.message.usage.input_tokens || 0;
1292
- const outputDelta = msg.message.usage.output_tokens || 0;
1293
- // Sanity check: max 100k tokens per message (generous limit)
1294
- const MAX_TOKENS_PER_MESSAGE = 100_000;
1295
- if (inputDelta > 0 && inputDelta <= MAX_TOKENS_PER_MESSAGE) {
1296
- this._totalInputTokens += inputDelta;
1297
- }
1298
- if (outputDelta > 0 && outputDelta <= MAX_TOKENS_PER_MESSAGE) {
1299
- this._totalOutputTokens += outputDelta;
1300
- }
1301
- // Check if we should auto-compact or auto-clear
1302
- this._autoOps.checkAutoCompact();
1303
- this._autoOps.checkAutoClear();
1304
- }
1305
- }
1306
- if (msg.type === 'result' && msg.total_cost_usd) {
1307
- this._totalCost = msg.total_cost_usd;
1308
- }
1309
- }
1310
- catch (parseErr) {
1311
- // Not JSON, just regular output - this is expected for non-JSON lines
1312
- console.debug('[Session] Line not JSON (expected for text output):', parseErr instanceof Error ? parseErr.message : parseErr);
1313
- this._textOutput.append(line + '\n');
1314
- }
1309
+ this._handleJsonMessage(cleanLine, line);
1315
1310
  }
1316
1311
  else if (trimmed) {
1317
1312
  this._textOutput.append(line + '\n');
@@ -1572,7 +1567,7 @@ export class Session extends EventEmitter {
1572
1567
  this._status = 'busy';
1573
1568
  this._lastActivityAt = Date.now();
1574
1569
  this.runPrompt(input).catch((err) => {
1575
- const errorMsg = err instanceof Error ? err.message : String(err);
1570
+ const errorMsg = getErrorMessage(err);
1576
1571
  // Clean up task state so the task queue doesn't get stuck
1577
1572
  if (this._currentTaskId) {
1578
1573
  const taskId = this._currentTaskId;
@@ -1642,38 +1637,7 @@ export class Session extends EventEmitter {
1642
1637
  async stop(killMux = true) {
1643
1638
  // Set stopped flag first to prevent new timers from being created
1644
1639
  this._isStopped = true;
1645
- // Clear activity timeout to prevent memory leak
1646
- if (this.activityTimeout) {
1647
- clearTimeout(this.activityTimeout);
1648
- this.activityTimeout = null;
1649
- }
1650
- // Clear line buffer flush timer
1651
- if (this._lineBufferFlushTimer) {
1652
- clearTimeout(this._lineBufferFlushTimer);
1653
- this._lineBufferFlushTimer = null;
1654
- }
1655
- // Destroy auto-compact/auto-clear automation (clears its timers)
1656
- this._autoOps.destroy();
1657
- // Clear prompt check timers
1658
- if (this._promptCheckInterval) {
1659
- clearInterval(this._promptCheckInterval);
1660
- this._promptCheckInterval = null;
1661
- }
1662
- if (this._promptCheckTimeout) {
1663
- clearTimeout(this._promptCheckTimeout);
1664
- this._promptCheckTimeout = null;
1665
- }
1666
- // Clear shell idle timer
1667
- if (this._shellIdleTimer) {
1668
- clearTimeout(this._shellIdleTimer);
1669
- this._shellIdleTimer = null;
1670
- }
1671
- // Clear expensive processing timer
1672
- if (this._expensiveProcessTimer) {
1673
- clearTimeout(this._expensiveProcessTimer);
1674
- this._expensiveProcessTimer = null;
1675
- }
1676
- this._pendingCleanData = '';
1640
+ this._clearAllTimers();
1677
1641
  // Immediately cleanup Promise callbacks to prevent orphaned references
1678
1642
  // during the rest of stop() processing (e.g., if mux kill times out)
1679
1643
  if (this.rejectPromise && !this._promptResolved) {