coxpit 3.4.0 → 3.4.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/board.ts +14 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coxpit",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/board.ts CHANGED
@@ -1158,14 +1158,20 @@ let termRunId = null, termClosing = false, termRetry = 0, termRetryTimer = null;
1158
1158
  function termConnect(){
1159
1159
  if (termRunId==null || termClosing || !termObj) return;
1160
1160
  const proto = location.protocol==='https:'?'wss':'ws';
1161
- termWS = new WebSocket(proto+'://'+location.host+'/ws/term/'+termRunId
1161
+ // 소켓 정체성 가드 — close 이벤트는 비동기라 termClosing 토글만으론 못 막는다.
1162
+ // 대체된(sock!==termWS) 소켓은 출력도 재연결도 금지: 같은 tmux 세션에 WS 가
1163
+ // 누적 attach 되면 tmux 가 전 클라이언트에 미러링해 글자가 N번씩 보인다.
1164
+ const sock = new WebSocket(proto+'://'+location.host+'/ws/term/'+termRunId
1162
1165
  +'?cols='+termObj.cols+'&rows='+termObj.rows);
1163
- termWS.onopen = ()=>{
1166
+ termWS = sock;
1167
+ sock.onopen = ()=>{
1168
+ if (sock!==termWS){ try{ sock.close(); }catch{} return; }
1164
1169
  termRetry = 0;
1165
1170
  $('termTitle').textContent = ((runs.get(termRunId)||{}).tmuxWindow||'terminal');
1166
1171
  termObj.focus();
1167
1172
  };
1168
- termWS.onmessage = (m)=>{
1173
+ sock.onmessage = (m)=>{
1174
+ if (sock!==termWS || !termObj) return;
1169
1175
  try{
1170
1176
  const d = JSON.parse(m.data);
1171
1177
  if (d.t==='o') termObj.write(d.d);
@@ -1173,7 +1179,8 @@ function termConnect(){
1173
1179
  else if (d.t==='exit') termObj.write('\\r\\n\\x1b[90m[session ended — reconnecting will revive it]\\x1b[0m\\r\\n');
1174
1180
  }catch{}
1175
1181
  };
1176
- termWS.onclose = ()=>{
1182
+ sock.onclose = ()=>{
1183
+ if (sock!==termWS) return;
1177
1184
  if (termClosing || termRunId==null) return;
1178
1185
  // 예기치 않은 끊김 — 백오프 재연결(서버가 죽은 세션도 소생시킴)
1179
1186
  const delay = Math.min(8000, 800 * Math.pow(2, termRetry++));
@@ -1204,6 +1211,9 @@ function termSwitch(id){
1204
1211
  }
1205
1212
  function openTerm(runId){
1206
1213
  const r = runs.get(runId); if(!r) return;
1214
+ // 재진입 방어 — 이전 소켓/타이머가 남아 있으면 정리(중복 attach 방지)
1215
+ if (termRetryTimer){ clearTimeout(termRetryTimer); termRetryTimer=null; }
1216
+ if (termWS){ const old=termWS; termWS=null; try{ old.close(); }catch{} }
1207
1217
  termRunId = runId; termClosing = false; termRetry = 0;
1208
1218
  $('termRid').textContent = 'r'+runId;
1209
1219
  $('termTitle').textContent = (r.tmuxWindow||'terminal');