tide-commander 1.159.2 → 1.160.0
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/CHANGELOG.md +10 -0
- package/dist/assets/{BossLogsModal-D7DRbxuN.js → BossLogsModal-COg0RBw_.js} +1 -1
- package/dist/assets/{BossSpawnModal-qWqV6cXb.js → BossSpawnModal-Bvkk2uFi.js} +1 -1
- package/dist/assets/{ControlsModal-Dx4Sd-zd.js → ControlsModal-BWcWGs2x.js} +1 -1
- package/dist/assets/{DockerLogsModal-DnBm69YT.js → DockerLogsModal-C4tJ59Pf.js} +1 -1
- package/dist/assets/{EmbeddedEditor-egK0IcwH.js → EmbeddedEditor-DiDm5EvF.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-Bi51Jkf8.js → GmailOAuthSetup-GkYs6wUs.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BAREZ_c-.js → GoogleOAuthSetup-KKwp2gEQ.js} +1 -1
- package/dist/assets/{IframeModal-Rh1yC6CF.js → IframeModal-D0iTkxZB.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BPmG30Kf.js → IntegrationsPanel-CCKoizJa.js} +2 -2
- package/dist/assets/{LogViewerModal-V3LGKLOd.js → LogViewerModal-CSGEkyhZ.js} +1 -1
- package/dist/assets/{MonitoringModal-D5ObJhXH.js → MonitoringModal-Be6uEd7K.js} +1 -1
- package/dist/assets/{PM2LogsModal-DyNAmc5D.js → PM2LogsModal-DZG7Tcw7.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-C1afIOVX.js → RestoreArchivedAreaModal-CigBbBB_.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C7weuP-e.js → Scene2DCanvas-sJig3vJ4.js} +1 -1
- package/dist/assets/{SceneManager-Cub1EW5Y.js → SceneManager-BG_0UVJc.js} +1 -1
- package/dist/assets/{SkillsPanel-C3PG0A6A.js → SkillsPanel-BnAf8uyn.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-vq1FipyR.js → SlackMultiInstanceSetup-DGuexHRz.js} +1 -1
- package/dist/assets/{SpawnModal-Df2Vp-HA.js → SpawnModal-FjDJZlIm.js} +1 -1
- package/dist/assets/{StatisticsModal-D0yo2rbm.js → StatisticsModal-CbbdwSF0.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-BHQS1Xc_.js → SubordinateAssignmentModal-Dn2uuOcT.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-CEVMReTJ.js → TriggerManagerPanel-Cj9pqf-r.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-BQbr0HYZ.js → WorkflowEditorPanel-nucrx_0v.js} +1 -1
- package/dist/assets/{area-logos-CbdG7b49.js → area-logos-C93KZk3h.js} +1 -1
- package/dist/assets/{index-DN1jWADn.js → index-BkbKu66S.js} +1 -1
- package/dist/assets/{index-EAqEvdMn.js → index-CmmI2kZf.js} +2 -2
- package/dist/assets/{index-B4PPDHAS.js → index-D7YoMagO.js} +1 -1
- package/dist/assets/{index-BvEk2Spw.js → index-DIOhr3im.js} +3 -3
- package/dist/assets/{index-Vee1Cpl9.js → index-DqM0QltS.js} +1 -1
- package/dist/assets/{index--FFJ9DPS.js → index-Gjc7NXlZ.js} +1 -1
- package/dist/assets/{index-fFR_58oB.js → index-fh53j_4E.js} +1 -1
- package/dist/assets/{index-C57r58R1.js → index-iX9jPnEp.js} +1 -1
- package/dist/assets/{index-CxUhEi9L.js → index-ojmo-Cva.js} +1 -1
- package/dist/assets/{main-Btdl34Y5.js → main-BFW0rGXU.js} +8 -8
- package/dist/assets/{web-Brq808vy.js → web-8gl2noUE.js} +1 -1
- package/dist/assets/{web-CACmzQOn.js → web-D0uiHhnA.js} +1 -1
- package/dist/assets/{web-DPUDmaxp.js → web-M9cLwTrs.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/claude/runner/tmux-helper.js +80 -18
- package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +19 -4
- package/dist/src/packages/server/runtime/codex-runner-router.js +5 -1
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +5 -6
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{df as a}from"./main-
|
|
1
|
+
import{df as a}from"./main-BFW0rGXU.js";import{ImpactStyle as i,NotificationType as r}from"./index-CmmI2kZf.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-
|
|
1
|
+
import{df as s}from"./main-BFW0rGXU.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-
|
|
1
|
+
import{df as t}from"./main-BFW0rGXU.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};
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.160.0"}
|
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-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-BFW0rGXU.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
|
-
|
|
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', '
|
|
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
|
}
|
|
@@ -233,7 +233,10 @@ export class CodexAppServerRunner {
|
|
|
233
233
|
};
|
|
234
234
|
this.agents.set(agentId, state);
|
|
235
235
|
this.threadToAgent.set(threadId, agentId);
|
|
236
|
-
agentService.updateAgent(agentId, {
|
|
236
|
+
agentService.updateAgent(agentId, {
|
|
237
|
+
sessionId: threadId,
|
|
238
|
+
...(request.forkSession ? { forkSourceSessionId: undefined } : {}),
|
|
239
|
+
});
|
|
237
240
|
this.callbacks.onSessionId(agentId, threadId);
|
|
238
241
|
this.pipeline.emitStandardEvent(agentId, {
|
|
239
242
|
type: 'init',
|
|
@@ -261,17 +264,29 @@ export class CodexAppServerRunner {
|
|
|
261
264
|
baseParams.config = { model_reasoning_effort: effort };
|
|
262
265
|
try {
|
|
263
266
|
if (!wantNew && request.sessionId) {
|
|
264
|
-
const
|
|
267
|
+
const method = request.forkSession ? 'thread/fork' : 'thread/resume';
|
|
268
|
+
const result = (await proc.request(method, {
|
|
265
269
|
...baseParams,
|
|
266
270
|
threadId: request.sessionId,
|
|
267
271
|
}));
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
// A fork must always return its own id. Falling back to the source id
|
|
273
|
+
// would make both agents write to the same conversation.
|
|
274
|
+
const id = result?.thread?.id || (request.forkSession ? undefined : request.sessionId);
|
|
275
|
+
if (!id)
|
|
276
|
+
throw new Error(`${method} returned no thread id`);
|
|
277
|
+
log.log(`${request.forkSession ? '🌿 Forked' : '🔄 Resumed'} thread ${id.slice(0, 8)} for agent ${agentId.slice(0, 8)}`);
|
|
270
278
|
return id;
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
catch (err) {
|
|
274
282
|
const msg = err instanceof Error ? err.message : String(err);
|
|
283
|
+
if (request.forkSession) {
|
|
284
|
+
// Do not silently create an empty thread: keep forkSourceSessionId in
|
|
285
|
+
// place so the user can retry after upgrading/fixing Codex.
|
|
286
|
+
log.error(`thread/fork failed for ${agentId.slice(0, 8)}: ${msg}`);
|
|
287
|
+
this.callbacks.onError(agentId, `Codex session fork failed: ${msg}`);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
275
290
|
log.warn(`thread/resume failed for ${agentId.slice(0, 8)} (${msg}) — starting a fresh thread`);
|
|
276
291
|
}
|
|
277
292
|
try {
|
|
@@ -48,7 +48,11 @@ export class CodexRunnerRouter {
|
|
|
48
48
|
this.appServer.start?.();
|
|
49
49
|
}
|
|
50
50
|
async run(request) {
|
|
51
|
-
|
|
51
|
+
// `codex exec` can resume threads but has no headless fork command. The
|
|
52
|
+
// app-server does (`thread/fork`), so route the first turn of a fork through
|
|
53
|
+
// it regardless of the streaming preference. Later turns may switch back
|
|
54
|
+
// to exec and resume the newly-created thread normally.
|
|
55
|
+
const mode = request.forkSession ? 'app-server' : this.currentMode();
|
|
52
56
|
const prev = this.ownerOf(request.agentId);
|
|
53
57
|
if (prev && prev !== mode) {
|
|
54
58
|
log.log(`Agent ${request.agentId.slice(0, 8)} switching ${prev} → ${mode}; stopping old runner first`);
|
|
@@ -135,10 +135,9 @@ async function duplicateAgentConfig(source, name, position) {
|
|
|
135
135
|
function duplicateOffset(source) {
|
|
136
136
|
return { x: source.position.x + 2, y: 0, z: source.position.z + 2 };
|
|
137
137
|
}
|
|
138
|
-
// Providers whose conversation history can be forked
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const FORKABLE_PROVIDERS = new Set(['claude', 'opencode', 'grok']);
|
|
138
|
+
// Providers whose conversation history can be forked. Codex forks through the
|
|
139
|
+
// app-server `thread/fork` method even when regular turns use `codex exec`.
|
|
140
|
+
const FORKABLE_PROVIDERS = new Set(['claude', 'codex', 'opencode', 'grok']);
|
|
142
141
|
/**
|
|
143
142
|
* Handle clone_agent message - duplicates an existing agent's configuration
|
|
144
143
|
* into a brand-new agent (fresh session, no inherited context/memory).
|
|
@@ -168,9 +167,9 @@ export async function handleCloneAgent(ctx, payload) {
|
|
|
168
167
|
* Handle fork_agent message - like clone, but the new agent also CONTINUES the
|
|
169
168
|
* source's conversation history. On the new agent's first run the backend
|
|
170
169
|
* resumes the source session and forks it into a fresh one (Claude
|
|
171
|
-
* --fork-session / OpenCode --fork); the new session id is then captured and
|
|
170
|
+
* --fork-session / Codex thread/fork / OpenCode --fork); the new session id is then captured and
|
|
172
171
|
* forkSourceSessionId is cleared. If the source has no session or its provider
|
|
173
|
-
* isn't forkable
|
|
172
|
+
* isn't forkable, this degrades to a plain clone (fresh session).
|
|
174
173
|
*/
|
|
175
174
|
export async function handleForkAgent(ctx, payload) {
|
|
176
175
|
const source = agentService.getAgent(payload.sourceAgentId);
|