tide-commander 1.159.1 → 1.159.3

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 (42) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/{BossLogsModal-CL5C40v7.js → BossLogsModal-DII7aJf5.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-QRrIVSsE.js → BossSpawnModal-CQfEy11F.js} +1 -1
  4. package/dist/assets/{ControlsModal-_cw1dgUl.js → ControlsModal-DFo7lwSp.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-BMzpUMYF.js → DockerLogsModal-CXPYYhUg.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-DqY3hqbU.js → EmbeddedEditor-Dboxn7s4.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-Bc9srJE4.js → GmailOAuthSetup-BXsCPLsp.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-DeaxWPCd.js → GoogleOAuthSetup-Bl-vieRg.js} +1 -1
  9. package/dist/assets/{IframeModal-3uoqeRAN.js → IframeModal-BCRKWZQa.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-DL9VTiWv.js → IntegrationsPanel-C3Ia477x.js} +2 -2
  11. package/dist/assets/{LogViewerModal-Bp80nuou.js → LogViewerModal--Ij8llxK.js} +1 -1
  12. package/dist/assets/{MonitoringModal-p_ucVk30.js → MonitoringModal-OUrEUALC.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-Bk060m4u.js → PM2LogsModal-BIS752lh.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-DgYKagxt.js → RestoreArchivedAreaModal-07vSKn4W.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-C7Xc_5Dz.js → Scene2DCanvas-JDJNFXK3.js} +1 -1
  16. package/dist/assets/{SceneManager-D1HZULQL.js → SceneManager-CkUd1aHW.js} +1 -1
  17. package/dist/assets/{SkillsPanel-DR0YQBuD.js → SkillsPanel-DiLbHStJ.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-W9boSb9T.js → SlackMultiInstanceSetup-F17-870l.js} +1 -1
  19. package/dist/assets/{SpawnModal-CN0uPU2H.js → SpawnModal-DLmbxM30.js} +1 -1
  20. package/dist/assets/{StatisticsModal-Bs8WGrk8.js → StatisticsModal-l6Ed7xxb.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-CppZw5TE.js → SubordinateAssignmentModal-BNUrwWAx.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-BZeMe-9r.js → TriggerManagerPanel-DruR51gp.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-BH5HqvN2.js → WorkflowEditorPanel-D8O8NnFd.js} +1 -1
  24. package/dist/assets/{area-logos-B6CrA1uD.js → area-logos-Baka5-sr.js} +1 -1
  25. package/dist/assets/{index-B_RzzK2X.js → index-3mmNpr_o.js} +3 -3
  26. package/dist/assets/{index-DnOc4jz6.js → index-BEtUppej.js} +1 -1
  27. package/dist/assets/{index-BtnAoK73.js → index-C2F5Uash.js} +1 -1
  28. package/dist/assets/{index-ZZxeSHEQ.js → index-CGn4UG1-.js} +1 -1
  29. package/dist/assets/{index-CN1Rq7ZV.js → index-CifRfrvD.js} +1 -1
  30. package/dist/assets/{index-DI8A9Ggk.js → index-D89rSdZ_.js} +1 -1
  31. package/dist/assets/{index-D3ivqCnn.js → index-D_M5cgOI.js} +2 -2
  32. package/dist/assets/{index-Cmhnprql.js → index-LhjqVC1x.js} +1 -1
  33. package/dist/assets/{index-DHio1aFH.js → index-T8mnacEC.js} +1 -1
  34. package/dist/assets/{main-BbNRp516.js → main-Cn0uJsZU.js} +8 -8
  35. package/dist/assets/{web-BCXtfjWo.js → web-BLnXgjVn.js} +1 -1
  36. package/dist/assets/{web-C24Z_D9n.js → web-BjfQGXzj.js} +1 -1
  37. package/dist/assets/{web-CC__8ncF.js → web-DY_H8hM_.js} +1 -1
  38. package/dist/build-info.json +1 -1
  39. package/dist/index.html +1 -1
  40. package/dist/src/packages/server/claude/runner/tmux-helper.js +80 -18
  41. package/dist/src/packages/server/services/self-update-service.js +50 -8
  42. package/package.json +1 -1
@@ -1 +1 @@
1
- import{df as a}from"./main-BbNRp516.js";import{ImpactStyle as i,NotificationType as r}from"./index-D3ivqCnn.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{df as a}from"./main-Cn0uJsZU.js";import{ImpactStyle as i,NotificationType as r}from"./index-D_M5cgOI.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
@@ -1 +1 @@
1
- import{df as s}from"./main-BbNRp516.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{df as s}from"./main-Cn0uJsZU.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
@@ -1 +1 @@
1
- import{df as t}from"./main-BbNRp516.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
1
+ import{df as t}from"./main-Cn0uJsZU.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
@@ -1 +1 @@
1
- {"version":"1.159.1"}
1
+ {"version":"1.159.3"}
package/dist/index.html CHANGED
@@ -22,7 +22,7 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-BbNRp516.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-Cn0uJsZU.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
28
  <link rel="stylesheet" crossorigin href="/assets/main-BZcOdtO_.css">
@@ -17,6 +17,35 @@ import { createLogger } from '../../utils/logger.js';
17
17
  import { isTmuxModeEnabled } from '../../services/system-prompt-service.js';
18
18
  const log = createLogger('Tmux');
19
19
  // ---------------------------------------------------------------------------
20
+ // Environment sanitisation
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Strip the caller's tmux client vars before invoking tmux.
24
+ *
25
+ * If the commander itself was launched from inside a tmux pane (e.g. pm2 was
26
+ * started from an agent's own tmux session, and `pm2 save` then baked that env
27
+ * into the process definition), we inherit `TMUX=<socket>,<pid>,<pane>`.
28
+ * tmux takes its socket path from `$TMUX` when set — and, unlike the default
29
+ * path, it will NOT create the parent directory for it. After a reboot wipes
30
+ * /tmp, that socket dir is gone, so every `tmux new-session` fails with
31
+ * "error creating /tmp/tmux-1000/default (No such file or directory)"
32
+ * ...while still exiting 0. The session never starts, the agent's log stays
33
+ * empty, and the failure is completely silent.
34
+ *
35
+ * Dropping TMUX/TMUX_PANE makes tmux compute its own socket path and create
36
+ * the directory as needed, so spawning works regardless of how we were started.
37
+ */
38
+ function withoutTmuxClientVars(env) {
39
+ const clean = { ...env };
40
+ delete clean.TMUX;
41
+ delete clean.TMUX_PANE;
42
+ return clean;
43
+ }
44
+ /** Sanitised copy of the current process env, for tmux control commands. */
45
+ function tmuxEnv() {
46
+ return withoutTmuxClientVars(process.env);
47
+ }
48
+ // ---------------------------------------------------------------------------
20
49
  // Public helpers
21
50
  // ---------------------------------------------------------------------------
22
51
  /** Returns true when the user opted in (via Settings) AND tmux is available. */
@@ -64,7 +93,7 @@ export function spawnInTmux(executable, args, options) {
64
93
  fs.writeFileSync(stderrFile, '');
65
94
  // Kill any stale session with the same name (ignore errors)
66
95
  try {
67
- execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
96
+ execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
68
97
  }
69
98
  catch {
70
99
  // no existing session — that's fine
@@ -128,14 +157,46 @@ export function spawnInTmux(executable, args, options) {
128
157
  '--', 'sh', '-c', fullCmd,
129
158
  ], {
130
159
  cwd: options.cwd,
131
- env: options.env,
160
+ env: withoutTmuxClientVars(options.env),
132
161
  detached: true,
133
- stdio: ['ignore', 'ignore', 'ignore'],
162
+ // stderr is piped, not ignored: tmux reports fatal errors (bad socket
163
+ // dir, unusable session name) on stderr *and still exits 0*. Discarding
164
+ // it turned a failed spawn into a silent no-op that only surfaced later
165
+ // as a bogus "process died" from the watchdog.
166
+ stdio: ['ignore', 'ignore', 'pipe'],
134
167
  });
135
168
  launcherProcess.unref();
169
+ watchLauncherForFailure(launcherProcess, sessionName);
136
170
  log.log(`Spawned tmux session ${sessionName}: ${executable} ${escapedArgs} (stdout -> ${logFile})`);
137
171
  return { launcherProcess, sessionName, logFile };
138
172
  }
173
+ /**
174
+ * Surface a failed `tmux new-session`. tmux exits 0 even when it could not
175
+ * create the session, so the exit code alone tells us nothing — anything on
176
+ * stderr means the session did not start.
177
+ *
178
+ * Re-emits as an 'error' on the launcher so callers already listening for
179
+ * spawn failures report it, instead of waiting for the watchdog to misdiagnose
180
+ * the missing session as a crashed agent.
181
+ */
182
+ function watchLauncherForFailure(launcherProcess, sessionName) {
183
+ let stderr = '';
184
+ launcherProcess.stderr?.on('data', (chunk) => {
185
+ stderr += String(chunk);
186
+ });
187
+ launcherProcess.on('exit', (code) => {
188
+ const message = stderr.trim();
189
+ if (!message && code === 0) {
190
+ return;
191
+ }
192
+ const detail = message || `tmux exited with code ${code}`;
193
+ log.error(`Failed to create tmux session ${sessionName}: ${detail}`);
194
+ // Only emit if someone is listening — an unhandled 'error' event throws.
195
+ if (launcherProcess.listenerCount('error') > 0) {
196
+ launcherProcess.emit('error', new Error(`tmux new-session failed: ${detail}`));
197
+ }
198
+ });
199
+ }
139
200
  // ---------------------------------------------------------------------------
140
201
  // Sending input
141
202
  // ---------------------------------------------------------------------------
@@ -155,7 +216,7 @@ export function sendToTmux(agentId, text) {
155
216
  const tmpFile = path.join(os.tmpdir(), `tc-stdin-${agentId}.tmp`);
156
217
  const bufferName = `tc-input-${agentId}`;
157
218
  fs.writeFileSync(tmpFile, text + '\n');
158
- execSync(`tmux load-buffer -b ${bufferName} ${tmpFile} && tmux paste-buffer -b ${bufferName} -t ${sessionName} -r -d`, { stdio: 'ignore', timeout: 5000 });
219
+ execSync(`tmux load-buffer -b ${bufferName} ${tmpFile} && tmux paste-buffer -b ${bufferName} -t ${sessionName} -r -d`, { stdio: 'ignore', timeout: 5000, env: tmuxEnv() });
159
220
  fs.unlinkSync(tmpFile);
160
221
  return true;
161
222
  }
@@ -171,7 +232,7 @@ export function sendToTmux(agentId, text) {
171
232
  export function hasTmuxSession(agentId) {
172
233
  const sessionName = tmuxSessionName(agentId);
173
234
  try {
174
- execSync(`tmux has-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
235
+ execSync(`tmux has-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
175
236
  return true;
176
237
  }
177
238
  catch {
@@ -184,7 +245,7 @@ export function killTmuxSession(agentId) {
184
245
  const logFile = tmuxLogPath(agentId);
185
246
  const stderrFile = `${logFile}.stderr`;
186
247
  try {
187
- execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
248
+ execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
188
249
  log.log(`Killed tmux session ${sessionName}`);
189
250
  }
190
251
  catch {
@@ -208,7 +269,7 @@ export function killTmuxSession(agentId) {
208
269
  */
209
270
  export function listAgentTmuxSessions() {
210
271
  try {
211
- const out = execSync(`tmux list-sessions -F '#{session_name}' 2>/dev/null`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
272
+ const out = execSync(`tmux list-sessions -F '#{session_name}' 2>/dev/null`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: tmuxEnv() });
212
273
  return out
213
274
  .split('\n')
214
275
  .map((s) => s.trim())
@@ -226,7 +287,7 @@ export function listAgentTmuxSessions() {
226
287
  export function interruptTmuxSession(agentId) {
227
288
  const sessionName = tmuxSessionName(agentId);
228
289
  try {
229
- execSync(`tmux send-keys -t ${sessionName} C-c`, { stdio: 'ignore', timeout: 5000 });
290
+ execSync(`tmux send-keys -t ${sessionName} C-c`, { stdio: 'ignore', timeout: 5000, env: tmuxEnv() });
230
291
  return true;
231
292
  }
232
293
  catch {
@@ -351,7 +412,7 @@ export function createFileTailer(logFile, onLine) {
351
412
  export function getTmuxPanePid(agentId) {
352
413
  const sessionName = tmuxSessionName(agentId);
353
414
  try {
354
- const output = execSync(`tmux list-panes -t ${sessionName} -F '#{pane_pid}'`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
415
+ const output = execSync(`tmux list-panes -t ${sessionName} -F '#{pane_pid}'`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: tmuxEnv() }).trim();
355
416
  const pid = parseInt(output.split('\n')[0], 10);
356
417
  return Number.isFinite(pid) ? pid : undefined;
357
418
  }
@@ -438,7 +499,7 @@ export function spawnInTmuxInteractive(executable, args, options) {
438
499
  const sessionName = interactiveTmuxSessionName(options.agentId);
439
500
  // Kill any stale session with the same name (ignore errors)
440
501
  try {
441
- execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
502
+ execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
442
503
  }
443
504
  catch {
444
505
  // no existing session — that's fine
@@ -462,11 +523,12 @@ export function spawnInTmuxInteractive(executable, args, options) {
462
523
  '--', executable, ...args,
463
524
  ], {
464
525
  cwd: options.cwd,
465
- env: options.env,
526
+ env: withoutTmuxClientVars(options.env),
466
527
  detached: true,
467
- stdio: ['ignore', 'ignore', 'ignore'],
528
+ stdio: ['ignore', 'ignore', 'pipe'],
468
529
  });
469
530
  launcherProcess.unref();
531
+ watchLauncherForFailure(launcherProcess, sessionName);
470
532
  log.log(`Spawned interactive tmux session ${sessionName}: ${executable} ${args.join(' ')}`);
471
533
  return { launcherProcess, sessionName };
472
534
  }
@@ -489,7 +551,7 @@ export function sendPromptToTmuxInteractive(agentId, text) {
489
551
  // bracketed-paste mode before the submit keypress.
490
552
  execSync(`tmux load-buffer -b ${bufferName} ${tmpFile} && ` +
491
553
  `tmux paste-buffer -b ${bufferName} -t ${sessionName} -p -r -d && ` +
492
- `sleep 0.15 && tmux send-keys -t ${sessionName} Enter`, { stdio: 'ignore', timeout: 8000 });
554
+ `sleep 0.15 && tmux send-keys -t ${sessionName} Enter`, { stdio: 'ignore', timeout: 8000, env: tmuxEnv() });
493
555
  try {
494
556
  fs.unlinkSync(tmpFile);
495
557
  }
@@ -509,7 +571,7 @@ export function sendPromptToTmuxInteractive(agentId, text) {
509
571
  export function hasInteractiveTmuxSession(agentId) {
510
572
  const sessionName = interactiveTmuxSessionName(agentId);
511
573
  try {
512
- execSync(`tmux has-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
574
+ execSync(`tmux has-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
513
575
  return true;
514
576
  }
515
577
  catch {
@@ -520,7 +582,7 @@ export function hasInteractiveTmuxSession(agentId) {
520
582
  export function killInteractiveTmuxSession(agentId) {
521
583
  const sessionName = interactiveTmuxSessionName(agentId);
522
584
  try {
523
- execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore' });
585
+ execSync(`tmux kill-session -t ${sessionName} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
524
586
  log.log(`Killed interactive tmux session ${sessionName}`);
525
587
  }
526
588
  catch {
@@ -531,7 +593,7 @@ export function killInteractiveTmuxSession(agentId) {
531
593
  export function interruptInteractiveTmuxSession(agentId) {
532
594
  const sessionName = interactiveTmuxSessionName(agentId);
533
595
  try {
534
- execSync(`tmux send-keys -t ${sessionName} Escape`, { stdio: 'ignore', timeout: 5000 });
596
+ execSync(`tmux send-keys -t ${sessionName} Escape`, { stdio: 'ignore', timeout: 5000, env: tmuxEnv() });
535
597
  return true;
536
598
  }
537
599
  catch {
@@ -544,7 +606,7 @@ export function interruptInteractiveTmuxSession(agentId) {
544
606
  */
545
607
  export function listInteractiveTmuxSessions() {
546
608
  try {
547
- const out = execSync(`tmux list-sessions -F '#{session_name}' 2>/dev/null`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
609
+ const out = execSync(`tmux list-sessions -F '#{session_name}' 2>/dev/null`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: tmuxEnv() });
548
610
  return out
549
611
  .split('\n')
550
612
  .map((s) => s.trim())
@@ -559,7 +621,7 @@ export function listInteractiveTmuxSessions() {
559
621
  export function getInteractiveTmuxPanePid(agentId) {
560
622
  const sessionName = interactiveTmuxSessionName(agentId);
561
623
  try {
562
- const output = execSync(`tmux list-panes -t ${sessionName} -F '#{pane_pid}'`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
624
+ const output = execSync(`tmux list-panes -t ${sessionName} -F '#{pane_pid}'`, { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: tmuxEnv() }).trim();
563
625
  const pid = parseInt(output.split('\n')[0], 10);
564
626
  return Number.isFinite(pid) ? pid : undefined;
565
627
  }
@@ -228,21 +228,63 @@ export function runNpmGlobalUpdate(callbacks = {}) {
228
228
  });
229
229
  });
230
230
  }
231
+ /**
232
+ * Detect a process supervisor that already restarts us on exit.
233
+ *
234
+ * Under PM2 (the common `tide-commander --foreground` deployment) spawning our
235
+ * own detached relauncher is actively harmful: PM2 ALSO restarts the process the
236
+ * moment the old server exits, so the relauncher's new server and PM2's fresh
237
+ * server race for the same port. The loser dies with EADDRINUSE and — in
238
+ * containers whose PID 1 doesn't reap — lingers as a zombie that then trips the
239
+ * launcher's port guard on the next boot (the multi-day crash loop).
240
+ *
241
+ * PM2 injects pm_id / PM2_HOME into every managed process, and the launcher
242
+ * passes its env straight to the server child, so these are visible here. An
243
+ * explicit TIDE_COMMANDER_SUPERVISED=1 lets other auto-restarting supervisors
244
+ * (systemd Restart=, docker restart-policy) opt into the same behaviour.
245
+ *
246
+ * NOTE: we deliberately do NOT key on TIDE_COMMANDER_FOREGROUND — foreground is
247
+ * not the same as supervised. A bare `--foreground` attached in a terminal has
248
+ * nothing to restart it, so there the detached relauncher must stay.
249
+ */
250
+ function isSupervisedRestart() {
251
+ return (process.env.pm_id !== undefined ||
252
+ Boolean(process.env.PM2_HOME) ||
253
+ process.env.TIDE_COMMANDER_SUPERVISED === '1');
254
+ }
231
255
  /**
232
256
  * After a successful global update, bring up the freshly installed binary
233
257
  * without the user having to relaunch by hand.
234
258
  *
235
- * Spawns a fully detached `tide-commander start --restart` that inherits the
236
- * current server's environment (same PORT/HOST/AUTH_TOKEN/HTTPS). That process
237
- * SIGTERMs the still-running old server (triggering its graceful shutdown so the
238
- * port is released), waits for it to exit, then starts the new server and
239
- * rewrites the PID file. The caller MUST NOT exit — the relauncher drives the
240
- * handoff.
259
+ * When supervised (PM2/systemd/docker see isSupervisedRestart), we simply exit:
260
+ * the supervisor restarts the process into the already-on-disk new code, and
261
+ * spawning our own relauncher would only race it for the port.
262
+ *
263
+ * Otherwise (standalone install) spawns a fully detached
264
+ * `tide-commander start --restart` that inherits the current server's
265
+ * environment (same PORT/HOST/AUTH_TOKEN/HTTPS). That process SIGTERMs the
266
+ * still-running old server (triggering its graceful shutdown so the port is
267
+ * released), waits for it to exit, then starts the new server and rewrites the
268
+ * PID file. The caller MUST NOT exit — the relauncher (or, when supervised, our
269
+ * own scheduled exit) drives the handoff.
241
270
  *
242
- * Returns true if the relauncher was scheduled, false if it could not be
243
- * (in which case the caller should fall back to exiting for a manual restart).
271
+ * Returns true if a restart was scheduled (relauncher spawned, or supervised
272
+ * exit queued), false if it could not be (in which case the caller should fall
273
+ * back to exiting for a manual restart).
244
274
  */
245
275
  export function schedulePostUpdateRestart(delayMs = 600) {
276
+ // Supervised deployment: don't spawn a competing relauncher. Exit after the
277
+ // same flush grace so any in-flight SSE 'done' frame reaches the client, then
278
+ // let the process manager restart us into the freshly-installed build.
279
+ if (isSupervisedRestart()) {
280
+ log.log('Supervised restart (PM2/systemd/docker) detected — exiting for the process manager to restart into the updated build.');
281
+ const timer = setTimeout(() => {
282
+ log.log('Exiting now; the process manager will restart Tide Commander.');
283
+ process.exit(0);
284
+ }, delayMs);
285
+ timer.unref?.();
286
+ return true;
287
+ }
246
288
  let cliPath;
247
289
  try {
248
290
  // self-update-service.js lives at dist/.../server/services/, the CLI entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.159.1",
3
+ "version": "1.159.3",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",