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.
- package/dist/ai-checker-base.d.ts.map +1 -1
- package/dist/ai-checker-base.js +3 -2
- package/dist/ai-checker-base.js.map +1 -1
- package/dist/bash-tool-parser.d.ts +6 -0
- package/dist/bash-tool-parser.d.ts.map +1 -1
- package/dist/bash-tool-parser.js +87 -101
- package/dist/bash-tool-parser.js.map +1 -1
- package/dist/file-stream-manager.d.ts.map +1 -1
- package/dist/file-stream-manager.js +2 -1
- package/dist/file-stream-manager.js.map +1 -1
- package/dist/orchestrator-loop.d.ts +2 -0
- package/dist/orchestrator-loop.d.ts.map +1 -1
- package/dist/orchestrator-loop.js +27 -22
- package/dist/orchestrator-loop.js.map +1 -1
- package/dist/orchestrator-verifier.d.ts +1 -1
- package/dist/orchestrator-verifier.d.ts.map +1 -1
- package/dist/orchestrator-verifier.js +3 -2
- package/dist/orchestrator-verifier.js.map +1 -1
- package/dist/plan-orchestrator.d.ts +4 -1
- package/dist/plan-orchestrator.d.ts.map +1 -1
- package/dist/plan-orchestrator.js +66 -88
- package/dist/plan-orchestrator.js.map +1 -1
- package/dist/ralph-status-parser.d.ts +2 -0
- package/dist/ralph-status-parser.d.ts.map +1 -1
- package/dist/ralph-status-parser.js +98 -102
- package/dist/ralph-status-parser.js.map +1 -1
- package/dist/ralph-tracker.d.ts +9 -0
- package/dist/ralph-tracker.d.ts.map +1 -1
- package/dist/ralph-tracker.js +52 -60
- package/dist/ralph-tracker.js.map +1 -1
- package/dist/respawn-controller.d.ts +18 -1
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +215 -181
- package/dist/respawn-controller.js.map +1 -1
- package/dist/session-auto-ops.d.ts.map +1 -1
- package/dist/session-auto-ops.js +57 -55
- package/dist/session-auto-ops.js.map +1 -1
- package/dist/session.d.ts +5 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +182 -218
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +6 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +67 -79
- package/dist/state-store.js.map +1 -1
- package/dist/subagent-watcher.d.ts +24 -0
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +215 -220
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/tmux-manager.d.ts +17 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +57 -66
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tunnel-manager.d.ts.map +1 -1
- package/dist/tunnel-manager.js +2 -1
- package/dist/tunnel-manager.js.map +1 -1
- package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
- package/dist/web/public/app.16290ae3.js +26 -0
- package/dist/web/public/app.16290ae3.js.br +0 -0
- package/dist/web/public/app.16290ae3.js.gz +0 -0
- package/dist/web/public/constants.64161167.js.gz +0 -0
- package/dist/web/public/index.html +7 -7
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.88082175.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
- package/dist/web/public/mobile.0b213796.css.gz +0 -0
- package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.8204db1e.js → panels-ui.2d5b9703.js} +1 -1
- package/dist/web/public/panels-ui.2d5b9703.js.br +0 -0
- package/dist/web/public/panels-ui.2d5b9703.js.gz +0 -0
- package/dist/web/public/{ralph-panel.a2733fd5.js → ralph-panel.61076370.js} +1 -1
- package/dist/web/public/ralph-panel.61076370.js.br +0 -0
- package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
- package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
- package/dist/web/public/{respawn-ui.372c6ea7.js → respawn-ui.60be6ef5.js} +1 -1
- package/dist/web/public/respawn-ui.60be6ef5.js.br +0 -0
- package/dist/web/public/respawn-ui.60be6ef5.js.gz +0 -0
- package/dist/web/public/{session-ui.72f2f538.js → session-ui.554092ae.js} +1 -1
- package/dist/web/public/session-ui.554092ae.js.br +0 -0
- package/dist/web/public/session-ui.554092ae.js.gz +0 -0
- package/dist/web/public/{settings-ui.bd3eaadb.js → settings-ui.c58b0b9b.js} +7 -7
- package/dist/web/public/settings-ui.c58b0b9b.js.br +0 -0
- package/dist/web/public/settings-ui.c58b0b9b.js.gz +0 -0
- package/dist/web/public/styles.111ff326.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.474f79df.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/respawn-event-wiring.d.ts +51 -0
- package/dist/web/respawn-event-wiring.d.ts.map +1 -0
- package/dist/web/respawn-event-wiring.js +280 -0
- package/dist/web/respawn-event-wiring.js.map +1 -0
- package/dist/web/route-helpers.d.ts +23 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +53 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +2 -11
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +8 -24
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
- package/dist/web/routes/orchestrator-routes.js +23 -30
- package/dist/web/routes/orchestrator-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +17 -71
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/server.d.ts +4 -51
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +98 -941
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +89 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -0
- package/dist/web/session-listener-wiring.js +290 -0
- package/dist/web/session-listener-wiring.js.map +1 -0
- package/dist/web/sse-stream-manager.d.ts +91 -0
- package/dist/web/sse-stream-manager.d.ts.map +1 -0
- package/dist/web/sse-stream-manager.js +426 -0
- package/dist/web/sse-stream-manager.js.map +1 -0
- package/package.json +1 -1
- package/dist/web/public/app.e09fd4a6.js +0 -26
- package/dist/web/public/app.e09fd4a6.js.br +0 -0
- package/dist/web/public/app.e09fd4a6.js.gz +0 -0
- package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
- package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
- package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
- package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
- package/dist/web/public/respawn-ui.372c6ea7.js.br +0 -0
- package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
- package/dist/web/public/session-ui.72f2f538.js.br +0 -0
- package/dist/web/public/session-ui.72f2f538.js.gz +0 -0
- package/dist/web/public/settings-ui.bd3eaadb.js.br +0 -0
- 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.
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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) {
|