@agentuity/opencode 0.1.42 → 0.1.43
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/background/manager.d.ts +1 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +6 -0
- package/dist/background/manager.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +5 -0
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/tmux/executor.d.ts +43 -20
- package/dist/tmux/executor.d.ts.map +1 -1
- package/dist/tmux/executor.js +547 -243
- package/dist/tmux/executor.js.map +1 -1
- package/dist/tmux/index.d.ts +1 -1
- package/dist/tmux/index.d.ts.map +1 -1
- package/dist/tmux/index.js +1 -1
- package/dist/tmux/index.js.map +1 -1
- package/dist/tmux/manager.d.ts +37 -3
- package/dist/tmux/manager.d.ts.map +1 -1
- package/dist/tmux/manager.js +219 -96
- package/dist/tmux/manager.js.map +1 -1
- package/dist/tmux/types.d.ts +10 -0
- package/dist/tmux/types.d.ts.map +1 -1
- package/dist/tmux/types.js.map +1 -1
- package/dist/tmux/utils.d.ts +17 -0
- package/dist/tmux/utils.d.ts.map +1 -1
- package/dist/tmux/utils.js +39 -0
- package/dist/tmux/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/background/manager.ts +6 -0
- package/src/plugin/plugin.ts +5 -0
- package/src/tmux/executor.ts +610 -249
- package/src/tmux/index.ts +5 -2
- package/src/tmux/manager.ts +241 -98
- package/src/tmux/types.ts +11 -0
- package/src/tmux/utils.ts +39 -0
package/src/tmux/executor.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import type { PaneAction, WindowState, TmuxConfig } from './types';
|
|
2
2
|
import { runTmuxCommand, runTmuxCommandSync } from './utils';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
3
|
import { spawn, spawnSync } from 'bun';
|
|
7
4
|
|
|
8
|
-
/**
|
|
9
|
-
* Path to persist the agents window ID for crash recovery.
|
|
10
|
-
* Uses ~/.config/agentuity/coder/cache/ which is consistent with other Agentuity paths
|
|
11
|
-
* and likely exists for any Agentuity user.
|
|
12
|
-
*/
|
|
13
|
-
const CACHE_DIR = join(homedir(), '.config', 'agentuity', 'coder', 'cache');
|
|
14
|
-
const AGENTS_WINDOW_FILE = join(CACHE_DIR, 'agents-window-id');
|
|
15
|
-
|
|
16
5
|
/**
|
|
17
6
|
* Escape a string for safe use in shell commands.
|
|
18
7
|
* Wraps in single quotes and escapes any internal single quotes.
|
|
@@ -25,6 +14,25 @@ function shellEscape(str: string): string {
|
|
|
25
14
|
/** Maximum retries for recursive spawn attempts to prevent infinite loops */
|
|
26
15
|
const MAX_SPAWN_RETRIES = 3;
|
|
27
16
|
|
|
17
|
+
const OPENCODE_TAG = '@agentuity.opencode';
|
|
18
|
+
const OPENCODE_SERVER_TAG = '@agentuity.opencode.server';
|
|
19
|
+
const OPENCODE_OWNER_TAG = '@agentuity.opencode.ownerPid';
|
|
20
|
+
const OPENCODE_INSTANCE_TAG = '@agentuity.opencode.instance';
|
|
21
|
+
const OPENCODE_SESSION_TAG = '@agentuity.opencode.session';
|
|
22
|
+
|
|
23
|
+
type OwnershipContext = {
|
|
24
|
+
instanceId: string;
|
|
25
|
+
ownerPid: number;
|
|
26
|
+
serverKey: string;
|
|
27
|
+
tmuxSessionId?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type OwnershipLookup = {
|
|
31
|
+
serverKey: string;
|
|
32
|
+
instanceId: string;
|
|
33
|
+
tmuxSessionId?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
28
36
|
export interface ActionResult {
|
|
29
37
|
success: boolean;
|
|
30
38
|
paneId?: string;
|
|
@@ -45,7 +53,7 @@ function isProcessAlive(pid: number): boolean {
|
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
async function getPanePid(paneId: string): Promise<number | undefined> {
|
|
56
|
+
export async function getPanePid(paneId: string): Promise<number | undefined> {
|
|
49
57
|
if (!paneId) return undefined;
|
|
50
58
|
const result = await runTmuxCommand(['display', '-p', '-t', paneId, '#{pane_pid}']);
|
|
51
59
|
if (!result.success) return undefined;
|
|
@@ -54,6 +62,185 @@ async function getPanePid(paneId: string): Promise<number | undefined> {
|
|
|
54
62
|
return pid;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
export function getPanePidSync(paneId: string): number | undefined {
|
|
66
|
+
if (!paneId) return undefined;
|
|
67
|
+
const result = runTmuxCommandSync(['display', '-p', '-t', paneId, '#{pane_pid}']);
|
|
68
|
+
if (!result.success) return undefined;
|
|
69
|
+
const pid = Number(result.output.trim());
|
|
70
|
+
if (!Number.isFinite(pid) || pid <= 0) return undefined;
|
|
71
|
+
return pid;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function resolvePanePidWithRetry(
|
|
75
|
+
paneId: string,
|
|
76
|
+
maxAttempts = 3,
|
|
77
|
+
delayMs = 75
|
|
78
|
+
): Promise<number | undefined> {
|
|
79
|
+
let pid = await getPanePid(paneId);
|
|
80
|
+
if (pid) return pid;
|
|
81
|
+
for (let attempt = 1; attempt < maxAttempts; attempt += 1) {
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
83
|
+
pid = await getPanePid(paneId);
|
|
84
|
+
if (pid) return pid;
|
|
85
|
+
}
|
|
86
|
+
return pid;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function setWindowTags(windowId: string, ownership: OwnershipContext): Promise<void> {
|
|
90
|
+
await runTmuxCommand(['set-option', '-w', '-t', windowId, OPENCODE_TAG, '1']);
|
|
91
|
+
await runTmuxCommand([
|
|
92
|
+
'set-option',
|
|
93
|
+
'-w',
|
|
94
|
+
'-t',
|
|
95
|
+
windowId,
|
|
96
|
+
OPENCODE_SERVER_TAG,
|
|
97
|
+
ownership.serverKey,
|
|
98
|
+
]);
|
|
99
|
+
await runTmuxCommand([
|
|
100
|
+
'set-option',
|
|
101
|
+
'-w',
|
|
102
|
+
'-t',
|
|
103
|
+
windowId,
|
|
104
|
+
OPENCODE_OWNER_TAG,
|
|
105
|
+
String(ownership.ownerPid),
|
|
106
|
+
]);
|
|
107
|
+
await runTmuxCommand([
|
|
108
|
+
'set-option',
|
|
109
|
+
'-w',
|
|
110
|
+
'-t',
|
|
111
|
+
windowId,
|
|
112
|
+
OPENCODE_INSTANCE_TAG,
|
|
113
|
+
ownership.instanceId,
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function setPaneTags(
|
|
118
|
+
paneId: string,
|
|
119
|
+
ownership: OwnershipContext,
|
|
120
|
+
sessionId?: string
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
await runTmuxCommand([
|
|
123
|
+
'set-option',
|
|
124
|
+
'-p',
|
|
125
|
+
'-t',
|
|
126
|
+
paneId,
|
|
127
|
+
OPENCODE_INSTANCE_TAG,
|
|
128
|
+
ownership.instanceId,
|
|
129
|
+
]);
|
|
130
|
+
if (sessionId) {
|
|
131
|
+
await runTmuxCommand(['set-option', '-p', '-t', paneId, OPENCODE_SESSION_TAG, sessionId]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function findOwnedAgentsWindow(
|
|
136
|
+
serverKey: string,
|
|
137
|
+
instanceId: string,
|
|
138
|
+
tmuxSessionId?: string
|
|
139
|
+
): Promise<string | undefined> {
|
|
140
|
+
const result = await runTmuxCommand([
|
|
141
|
+
'list-windows',
|
|
142
|
+
'-a',
|
|
143
|
+
'-F',
|
|
144
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}\t#{session_id}`,
|
|
145
|
+
]);
|
|
146
|
+
if (!result.success || !result.output) return undefined;
|
|
147
|
+
|
|
148
|
+
for (const line of result.output.split('\n')) {
|
|
149
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId, sessionId] =
|
|
150
|
+
line.split('\t');
|
|
151
|
+
if (!windowId) continue;
|
|
152
|
+
if (windowName !== 'Agents') continue;
|
|
153
|
+
if (isOpencode !== '1') continue;
|
|
154
|
+
if (windowServerKey !== serverKey) continue;
|
|
155
|
+
if (windowInstanceId !== instanceId) continue;
|
|
156
|
+
// If tmuxSessionId is provided, only match windows in that session
|
|
157
|
+
if (tmuxSessionId && sessionId !== tmuxSessionId) continue;
|
|
158
|
+
return windowId;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function findOwnedAgentsWindowSync(
|
|
165
|
+
serverKey: string,
|
|
166
|
+
instanceId: string,
|
|
167
|
+
tmuxSessionId?: string
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const result = runTmuxCommandSync([
|
|
170
|
+
'list-windows',
|
|
171
|
+
'-a',
|
|
172
|
+
'-F',
|
|
173
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}\t#{session_id}`,
|
|
174
|
+
]);
|
|
175
|
+
if (!result.success || !result.output) return undefined;
|
|
176
|
+
|
|
177
|
+
for (const line of result.output.split('\n')) {
|
|
178
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId, sessionId] =
|
|
179
|
+
line.split('\t');
|
|
180
|
+
if (!windowId) continue;
|
|
181
|
+
if (windowName !== 'Agents') continue;
|
|
182
|
+
if (isOpencode !== '1') continue;
|
|
183
|
+
if (windowServerKey !== serverKey) continue;
|
|
184
|
+
if (windowInstanceId !== instanceId) continue;
|
|
185
|
+
// If tmuxSessionId is provided, only match windows in that session
|
|
186
|
+
if (tmuxSessionId && sessionId !== tmuxSessionId) continue;
|
|
187
|
+
return windowId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function findOwnedAgentPanes(
|
|
194
|
+
serverKey: string
|
|
195
|
+
): Promise<Array<{ paneId: string; panePid?: number; sessionId?: string; instanceId?: string }>> {
|
|
196
|
+
const windowsResult = await runTmuxCommand([
|
|
197
|
+
'list-windows',
|
|
198
|
+
'-a',
|
|
199
|
+
'-F',
|
|
200
|
+
`#{window_id}\t#{${OPENCODE_SERVER_TAG}}`,
|
|
201
|
+
]);
|
|
202
|
+
if (!windowsResult.success || !windowsResult.output) return [];
|
|
203
|
+
|
|
204
|
+
const serverWindowIds = new Set<string>();
|
|
205
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
206
|
+
const [windowId, windowServerKey] = line.split('\t');
|
|
207
|
+
if (!windowId) continue;
|
|
208
|
+
if (windowServerKey !== serverKey) continue;
|
|
209
|
+
serverWindowIds.add(windowId);
|
|
210
|
+
}
|
|
211
|
+
if (serverWindowIds.size === 0) return [];
|
|
212
|
+
|
|
213
|
+
const result = await runTmuxCommand([
|
|
214
|
+
'list-panes',
|
|
215
|
+
'-a',
|
|
216
|
+
'-F',
|
|
217
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}\t#{${OPENCODE_SESSION_TAG}}`,
|
|
218
|
+
]);
|
|
219
|
+
if (!result.success || !result.output) return [];
|
|
220
|
+
|
|
221
|
+
const panes: Array<{
|
|
222
|
+
paneId: string;
|
|
223
|
+
panePid?: number;
|
|
224
|
+
sessionId?: string;
|
|
225
|
+
instanceId?: string;
|
|
226
|
+
}> = [];
|
|
227
|
+
|
|
228
|
+
for (const line of result.output.split('\n')) {
|
|
229
|
+
const [paneId, panePidRaw, windowId, paneInstanceId, paneSessionId] = line.split('\t');
|
|
230
|
+
if (!paneId) continue;
|
|
231
|
+
if (!windowId || !serverWindowIds.has(windowId)) continue;
|
|
232
|
+
const panePid = Number(panePidRaw);
|
|
233
|
+
panes.push({
|
|
234
|
+
paneId,
|
|
235
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
236
|
+
sessionId: paneSessionId || undefined,
|
|
237
|
+
instanceId: paneInstanceId || undefined,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return panes;
|
|
242
|
+
}
|
|
243
|
+
|
|
57
244
|
/**
|
|
58
245
|
* Kill a process and all its children (the entire process tree).
|
|
59
246
|
*
|
|
@@ -114,53 +301,34 @@ export async function killProcessByPid(pid: number): Promise<boolean> {
|
|
|
114
301
|
|
|
115
302
|
/**
|
|
116
303
|
* State for separate-window mode - tracks the dedicated "Agents" window
|
|
304
|
+
* Keyed by `${serverKey}:${instanceId}:${tmuxSessionId}` to support multiple
|
|
305
|
+
* opencode instances running in different tmux sessions.
|
|
117
306
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Ensure the cache directory exists
|
|
122
|
-
*/
|
|
123
|
-
function ensureCacheDir(): void {
|
|
124
|
-
if (!existsSync(CACHE_DIR)) {
|
|
125
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
307
|
+
const agentsWindowIdByKey = new Map<string, string>();
|
|
128
308
|
|
|
129
309
|
/**
|
|
130
|
-
*
|
|
310
|
+
* Get the cache key for the agents window
|
|
131
311
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
ensureCacheDir();
|
|
135
|
-
writeFileSync(AGENTS_WINDOW_FILE, windowId, 'utf-8');
|
|
136
|
-
} catch {
|
|
137
|
-
// Ignore write errors - persistence is best-effort
|
|
138
|
-
}
|
|
312
|
+
function getAgentsWindowCacheKey(ownership: OwnershipContext): string {
|
|
313
|
+
return `${ownership.serverKey}:${ownership.instanceId}:${ownership.tmuxSessionId ?? 'default'}`;
|
|
139
314
|
}
|
|
140
315
|
|
|
141
316
|
/**
|
|
142
|
-
*
|
|
317
|
+
* Get the cached agents window ID for the given ownership context
|
|
143
318
|
*/
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
if (!existsSync(AGENTS_WINDOW_FILE)) return undefined;
|
|
147
|
-
const windowId = readFileSync(AGENTS_WINDOW_FILE, 'utf-8').trim();
|
|
148
|
-
return windowId || undefined;
|
|
149
|
-
} catch {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
319
|
+
function getCachedAgentsWindowId(ownership: OwnershipContext): string | undefined {
|
|
320
|
+
return agentsWindowIdByKey.get(getAgentsWindowCacheKey(ownership));
|
|
152
321
|
}
|
|
153
322
|
|
|
154
323
|
/**
|
|
155
|
-
*
|
|
324
|
+
* Set the cached agents window ID for the given ownership context
|
|
156
325
|
*/
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Ignore delete errors
|
|
326
|
+
function setCachedAgentsWindowId(ownership: OwnershipContext, windowId: string | undefined): void {
|
|
327
|
+
const key = getAgentsWindowCacheKey(ownership);
|
|
328
|
+
if (windowId) {
|
|
329
|
+
agentsWindowIdByKey.set(key, windowId);
|
|
330
|
+
} else {
|
|
331
|
+
agentsWindowIdByKey.delete(key);
|
|
164
332
|
}
|
|
165
333
|
}
|
|
166
334
|
|
|
@@ -171,11 +339,19 @@ function clearPersistedAgentsWindowId(): void {
|
|
|
171
339
|
*/
|
|
172
340
|
export async function executeAction(
|
|
173
341
|
action: PaneAction,
|
|
174
|
-
ctx: {
|
|
342
|
+
ctx: {
|
|
343
|
+
config: TmuxConfig;
|
|
344
|
+
serverUrl: string;
|
|
345
|
+
windowState: WindowState;
|
|
346
|
+
ownership: OwnershipContext;
|
|
347
|
+
}
|
|
175
348
|
): Promise<ActionResult> {
|
|
176
349
|
switch (action.type) {
|
|
177
350
|
case 'spawn':
|
|
178
|
-
return spawnInAgentsWindow(action, {
|
|
351
|
+
return spawnInAgentsWindow(action, {
|
|
352
|
+
serverUrl: ctx.serverUrl,
|
|
353
|
+
ownership: ctx.ownership,
|
|
354
|
+
});
|
|
179
355
|
case 'close':
|
|
180
356
|
return closePane(action);
|
|
181
357
|
case 'replace':
|
|
@@ -188,7 +364,12 @@ export async function executeAction(
|
|
|
188
364
|
*/
|
|
189
365
|
export async function executeActions(
|
|
190
366
|
actions: PaneAction[],
|
|
191
|
-
ctx: {
|
|
367
|
+
ctx: {
|
|
368
|
+
config: TmuxConfig;
|
|
369
|
+
serverUrl: string;
|
|
370
|
+
windowState: WindowState;
|
|
371
|
+
ownership: OwnershipContext;
|
|
372
|
+
}
|
|
192
373
|
): Promise<{
|
|
193
374
|
success: boolean;
|
|
194
375
|
spawnedPaneId?: string;
|
|
@@ -226,7 +407,7 @@ async function closePane(action: Extract<PaneAction, { type: 'close' }>): Promis
|
|
|
226
407
|
export async function closePaneById(paneId: string, pid?: number): Promise<ActionResult> {
|
|
227
408
|
let resolvedPid = pid;
|
|
228
409
|
if (!resolvedPid) {
|
|
229
|
-
resolvedPid = await
|
|
410
|
+
resolvedPid = await resolvePanePidWithRetry(paneId);
|
|
230
411
|
}
|
|
231
412
|
|
|
232
413
|
if (resolvedPid) {
|
|
@@ -246,18 +427,21 @@ export async function closePaneById(paneId: string, pid?: number): Promise<Actio
|
|
|
246
427
|
*/
|
|
247
428
|
async function replacePane(
|
|
248
429
|
action: Extract<PaneAction, { type: 'replace' }>,
|
|
249
|
-
ctx: { serverUrl: string }
|
|
430
|
+
ctx: { serverUrl: string; ownership: OwnershipContext }
|
|
250
431
|
): Promise<ActionResult> {
|
|
251
|
-
//
|
|
432
|
+
// Use exec to replace bash with opencode attach directly.
|
|
433
|
+
// This ensures signals go directly to opencode attach (no wrapper process).
|
|
434
|
+
// When opencode attach exits, the pane closes automatically (tmux remain-on-exit off).
|
|
252
435
|
// Use shellEscape to prevent shell injection via session IDs
|
|
253
436
|
const escapedServerUrl = shellEscape(ctx.serverUrl);
|
|
254
437
|
const escapedSessionId = shellEscape(action.newSessionId);
|
|
255
|
-
const command = `opencode attach ${escapedServerUrl} --session ${escapedSessionId}
|
|
438
|
+
const command = `exec opencode attach ${escapedServerUrl} --session ${escapedSessionId}`;
|
|
256
439
|
const result = await runTmuxCommand(['respawn-pane', '-k', '-t', action.paneId, command]);
|
|
257
440
|
if (!result.success) {
|
|
258
441
|
return { success: false, error: result.output };
|
|
259
442
|
}
|
|
260
|
-
|
|
443
|
+
await setPaneTags(action.paneId, ctx.ownership, action.newSessionId);
|
|
444
|
+
const pid = await resolvePanePidWithRetry(action.paneId);
|
|
261
445
|
return { success: true, paneId: action.paneId, pid };
|
|
262
446
|
}
|
|
263
447
|
|
|
@@ -275,7 +459,7 @@ async function replacePane(
|
|
|
275
459
|
*/
|
|
276
460
|
async function spawnInAgentsWindow(
|
|
277
461
|
action: Extract<PaneAction, { type: 'spawn' }>,
|
|
278
|
-
ctx: { serverUrl: string },
|
|
462
|
+
ctx: { serverUrl: string; ownership: OwnershipContext },
|
|
279
463
|
retryCount = 0
|
|
280
464
|
): Promise<ActionResult> {
|
|
281
465
|
// Prevent infinite recursion if tmux keeps failing
|
|
@@ -286,41 +470,77 @@ async function spawnInAgentsWindow(
|
|
|
286
470
|
};
|
|
287
471
|
}
|
|
288
472
|
|
|
289
|
-
//
|
|
473
|
+
// Use exec to replace bash with opencode attach directly.
|
|
474
|
+
// This ensures signals go directly to opencode attach (no wrapper process).
|
|
475
|
+
// When opencode attach exits, the pane closes automatically (tmux remain-on-exit off).
|
|
290
476
|
// Use shellEscape to prevent shell injection via session IDs
|
|
291
477
|
const escapedServerUrl = shellEscape(ctx.serverUrl);
|
|
292
478
|
const escapedSessionId = shellEscape(action.sessionId);
|
|
293
|
-
const command = `opencode attach ${escapedServerUrl} --session ${escapedSessionId}
|
|
479
|
+
const command = `exec opencode attach ${escapedServerUrl} --session ${escapedSessionId}`;
|
|
294
480
|
const layout = 'tiled'; // Always use tiled layout for grid arrangement
|
|
295
481
|
|
|
296
482
|
// Check if we have a cached agents window ID and if it still exists
|
|
297
|
-
|
|
483
|
+
let cachedWindowId = getCachedAgentsWindowId(ctx.ownership);
|
|
484
|
+
if (cachedWindowId) {
|
|
298
485
|
const checkResult = await runTmuxCommand([
|
|
299
|
-
'
|
|
486
|
+
'display',
|
|
487
|
+
'-p',
|
|
300
488
|
'-t',
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
'#{pane_id}',
|
|
489
|
+
cachedWindowId,
|
|
490
|
+
`#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
304
491
|
]);
|
|
305
492
|
|
|
306
|
-
if (!checkResult.success) {
|
|
307
|
-
|
|
308
|
-
|
|
493
|
+
if (!checkResult.success || !checkResult.output) {
|
|
494
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
495
|
+
cachedWindowId = undefined;
|
|
496
|
+
} else {
|
|
497
|
+
const [isOpencode, windowServerKey, windowInstanceId] = checkResult.output
|
|
498
|
+
.trim()
|
|
499
|
+
.split('\t');
|
|
500
|
+
if (
|
|
501
|
+
isOpencode !== '1' ||
|
|
502
|
+
windowServerKey !== ctx.ownership.serverKey ||
|
|
503
|
+
windowInstanceId !== ctx.ownership.instanceId
|
|
504
|
+
) {
|
|
505
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
506
|
+
cachedWindowId = undefined;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!cachedWindowId) {
|
|
512
|
+
cachedWindowId = await findOwnedAgentsWindow(
|
|
513
|
+
ctx.ownership.serverKey,
|
|
514
|
+
ctx.ownership.instanceId,
|
|
515
|
+
ctx.ownership.tmuxSessionId
|
|
516
|
+
);
|
|
517
|
+
if (cachedWindowId) {
|
|
518
|
+
setCachedAgentsWindowId(ctx.ownership, cachedWindowId);
|
|
309
519
|
}
|
|
310
520
|
}
|
|
311
521
|
|
|
312
522
|
// If no agents window exists, create one
|
|
313
|
-
if (!
|
|
314
|
-
|
|
315
|
-
|
|
523
|
+
if (!cachedWindowId) {
|
|
524
|
+
// Build the new-window command args
|
|
525
|
+
// CRITICAL: Use -t <session>: to target the correct tmux session
|
|
526
|
+
// Without this, tmux may create the window in the wrong session when
|
|
527
|
+
// multiple opencode instances run in different tmux sessions
|
|
528
|
+
const newWindowArgs = ['new-window'];
|
|
529
|
+
if (ctx.ownership.tmuxSessionId) {
|
|
530
|
+
// Target the specific tmux session (the colon after session_id is important)
|
|
531
|
+
newWindowArgs.push('-t', `${ctx.ownership.tmuxSessionId}:`);
|
|
532
|
+
}
|
|
533
|
+
newWindowArgs.push(
|
|
316
534
|
'-d', // Don't switch to new window
|
|
317
535
|
'-P',
|
|
318
536
|
'-F',
|
|
319
537
|
'#{window_id}:#{pane_id}',
|
|
320
538
|
'-n',
|
|
321
539
|
'Agents',
|
|
322
|
-
command
|
|
323
|
-
|
|
540
|
+
command
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const createResult = await runTmuxCommand(newWindowArgs);
|
|
324
544
|
|
|
325
545
|
if (!createResult.success) {
|
|
326
546
|
return { success: false, error: createResult.output };
|
|
@@ -329,19 +549,23 @@ async function spawnInAgentsWindow(
|
|
|
329
549
|
// Parse window_id:pane_id from output
|
|
330
550
|
const output = createResult.output?.trim() || '';
|
|
331
551
|
const [windowId, paneId] = output.split(':');
|
|
332
|
-
|
|
552
|
+
cachedWindowId = windowId;
|
|
333
553
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
554
|
+
if (cachedWindowId) {
|
|
555
|
+
setCachedAgentsWindowId(ctx.ownership, cachedWindowId);
|
|
556
|
+
await setWindowTags(cachedWindowId, ctx.ownership);
|
|
337
557
|
}
|
|
338
558
|
|
|
339
559
|
// Apply initial layout (useful when more panes are added later)
|
|
340
|
-
if (
|
|
341
|
-
await runTmuxCommand(['select-layout', '-t',
|
|
560
|
+
if (cachedWindowId && layout) {
|
|
561
|
+
await runTmuxCommand(['select-layout', '-t', cachedWindowId, layout]);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (paneId) {
|
|
565
|
+
await setPaneTags(paneId, ctx.ownership, action.sessionId);
|
|
342
566
|
}
|
|
343
567
|
|
|
344
|
-
const pid = paneId ? await
|
|
568
|
+
const pid = paneId ? await resolvePanePidWithRetry(paneId) : undefined;
|
|
345
569
|
return { success: true, paneId, windowId, pid };
|
|
346
570
|
}
|
|
347
571
|
|
|
@@ -350,21 +574,21 @@ async function spawnInAgentsWindow(
|
|
|
350
574
|
const listResult = await runTmuxCommand([
|
|
351
575
|
'list-panes',
|
|
352
576
|
'-t',
|
|
353
|
-
|
|
577
|
+
cachedWindowId,
|
|
354
578
|
'-F',
|
|
355
579
|
'#{pane_id}',
|
|
356
580
|
]);
|
|
357
581
|
|
|
358
582
|
if (!listResult.success || !listResult.output) {
|
|
359
583
|
// Fallback: create new window (with retry counter)
|
|
360
|
-
|
|
584
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
361
585
|
return spawnInAgentsWindow(action, ctx, retryCount + 1);
|
|
362
586
|
}
|
|
363
587
|
|
|
364
588
|
const targetPaneId = listResult.output.split('\n')[0]?.trim();
|
|
365
589
|
if (!targetPaneId) {
|
|
366
590
|
// Fallback: create new window (with retry counter)
|
|
367
|
-
|
|
591
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
368
592
|
return spawnInAgentsWindow(action, ctx, retryCount + 1);
|
|
369
593
|
}
|
|
370
594
|
|
|
@@ -385,234 +609,371 @@ async function spawnInAgentsWindow(
|
|
|
385
609
|
}
|
|
386
610
|
|
|
387
611
|
const paneId = splitResult.output?.trim();
|
|
612
|
+
if (cachedWindowId) {
|
|
613
|
+
await setWindowTags(cachedWindowId, ctx.ownership);
|
|
614
|
+
}
|
|
615
|
+
if (paneId) {
|
|
616
|
+
await setPaneTags(paneId, ctx.ownership, action.sessionId);
|
|
617
|
+
}
|
|
388
618
|
|
|
389
619
|
// Apply the configured layout to the agents window (e.g., tiled for grid)
|
|
390
|
-
if (
|
|
391
|
-
await runTmuxCommand(['select-layout', '-t',
|
|
620
|
+
if (cachedWindowId && layout) {
|
|
621
|
+
await runTmuxCommand(['select-layout', '-t', cachedWindowId, layout]);
|
|
392
622
|
}
|
|
393
623
|
|
|
394
|
-
const pid = paneId ? await
|
|
624
|
+
const pid = paneId ? await resolvePanePidWithRetry(paneId) : undefined;
|
|
395
625
|
return {
|
|
396
626
|
success: true,
|
|
397
627
|
paneId: paneId || undefined,
|
|
398
|
-
windowId:
|
|
628
|
+
windowId: cachedWindowId,
|
|
399
629
|
pid,
|
|
400
630
|
};
|
|
401
631
|
}
|
|
402
632
|
|
|
633
|
+
function killProcessByPidSync(pid: number): void {
|
|
634
|
+
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
spawnSync(['pkill', '-TERM', '-P', String(pid)]);
|
|
638
|
+
} catch {
|
|
639
|
+
// Ignore errors - children may not exist
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
process.kill(pid, 'SIGTERM');
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
646
|
+
if (code === 'ESRCH') return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const buffer = new SharedArrayBuffer(4);
|
|
651
|
+
const view = new Int32Array(buffer);
|
|
652
|
+
Atomics.wait(view, 0, 0, PROCESS_TERM_WAIT_MS);
|
|
653
|
+
} catch {
|
|
654
|
+
// ignore sleep errors
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
process.kill(pid, 0);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
661
|
+
if (code === 'ESRCH') return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
spawnSync(['pkill', '-KILL', '-P', String(pid)]);
|
|
666
|
+
} catch {
|
|
667
|
+
// Ignore errors
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
process.kill(pid, 'SIGKILL');
|
|
672
|
+
} catch {
|
|
673
|
+
// ignore errors
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
403
677
|
/**
|
|
404
678
|
* Reset the agents window state (for cleanup)
|
|
405
679
|
*/
|
|
406
|
-
export function resetAgentsWindow(): void {
|
|
407
|
-
|
|
408
|
-
|
|
680
|
+
export function resetAgentsWindow(ownership?: OwnershipContext): void {
|
|
681
|
+
if (ownership) {
|
|
682
|
+
setCachedAgentsWindowId(ownership, undefined);
|
|
683
|
+
} else {
|
|
684
|
+
agentsWindowIdByKey.clear();
|
|
685
|
+
}
|
|
409
686
|
}
|
|
410
687
|
|
|
411
688
|
/**
|
|
412
689
|
* Close the agents window if it exists
|
|
413
690
|
* This kills the entire window, which closes all panes within it
|
|
691
|
+
*
|
|
692
|
+
* SAFETY: Verifies the window is named "Agents" before killing to prevent
|
|
693
|
+
* accidentally killing user windows if the cached ID is stale.
|
|
414
694
|
*/
|
|
415
|
-
export async function closeAgentsWindow(): Promise<void> {
|
|
416
|
-
//
|
|
417
|
-
const
|
|
695
|
+
export async function closeAgentsWindow(ownership?: OwnershipLookup): Promise<void> {
|
|
696
|
+
// Build a pseudo-ownership context for cache lookup
|
|
697
|
+
const cacheKey = ownership
|
|
698
|
+
? {
|
|
699
|
+
serverKey: ownership.serverKey,
|
|
700
|
+
instanceId: ownership.instanceId,
|
|
701
|
+
ownerPid: 0,
|
|
702
|
+
tmuxSessionId: ownership.tmuxSessionId,
|
|
703
|
+
}
|
|
704
|
+
: undefined;
|
|
705
|
+
const cachedId = cacheKey ? getCachedAgentsWindowId(cacheKey) : undefined;
|
|
706
|
+
const windowId =
|
|
707
|
+
cachedId ??
|
|
708
|
+
(ownership
|
|
709
|
+
? await findOwnedAgentsWindow(
|
|
710
|
+
ownership.serverKey,
|
|
711
|
+
ownership.instanceId,
|
|
712
|
+
ownership.tmuxSessionId
|
|
713
|
+
)
|
|
714
|
+
: undefined);
|
|
418
715
|
if (!windowId) return;
|
|
419
716
|
|
|
420
|
-
|
|
717
|
+
const checkFormat = ownership
|
|
718
|
+
? `#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`
|
|
719
|
+
: '#{window_name}';
|
|
720
|
+
const checkResult = await runTmuxCommand(['display', '-p', '-t', windowId, checkFormat]);
|
|
721
|
+
|
|
722
|
+
if (!checkResult.success) {
|
|
723
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const parts = checkResult.output?.trim().split('\t') ?? [];
|
|
728
|
+
const windowName = parts[0];
|
|
729
|
+
if (windowName !== 'Agents') {
|
|
730
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (ownership) {
|
|
734
|
+
const [, isOpencode, windowServerKey, windowInstanceId] = parts;
|
|
735
|
+
if (
|
|
736
|
+
isOpencode !== '1' ||
|
|
737
|
+
windowServerKey !== ownership.serverKey ||
|
|
738
|
+
windowInstanceId !== ownership.instanceId
|
|
739
|
+
) {
|
|
740
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
421
745
|
await runTmuxCommand(['kill-window', '-t', windowId]);
|
|
422
|
-
|
|
423
|
-
clearPersistedAgentsWindowId();
|
|
746
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
424
747
|
}
|
|
425
748
|
|
|
426
749
|
/**
|
|
427
750
|
* Synchronously close the agents window (for shutdown)
|
|
428
|
-
* Uses
|
|
751
|
+
* Uses runTmuxCommandSync to ensure it completes before process exit
|
|
752
|
+
*
|
|
753
|
+
* SAFETY: Verifies the window is named "Agents" before killing to prevent
|
|
754
|
+
* accidentally killing user windows if the cached ID is stale.
|
|
429
755
|
*/
|
|
430
|
-
export function closeAgentsWindowSync(): void {
|
|
431
|
-
//
|
|
432
|
-
const
|
|
756
|
+
export function closeAgentsWindowSync(ownership?: OwnershipLookup): void {
|
|
757
|
+
// Build a pseudo-ownership context for cache lookup
|
|
758
|
+
const cacheKey = ownership
|
|
759
|
+
? {
|
|
760
|
+
serverKey: ownership.serverKey,
|
|
761
|
+
instanceId: ownership.instanceId,
|
|
762
|
+
ownerPid: 0,
|
|
763
|
+
tmuxSessionId: ownership.tmuxSessionId,
|
|
764
|
+
}
|
|
765
|
+
: undefined;
|
|
766
|
+
const cachedId = cacheKey ? getCachedAgentsWindowId(cacheKey) : undefined;
|
|
767
|
+
const windowId =
|
|
768
|
+
cachedId ??
|
|
769
|
+
(ownership
|
|
770
|
+
? findOwnedAgentsWindowSync(
|
|
771
|
+
ownership.serverKey,
|
|
772
|
+
ownership.instanceId,
|
|
773
|
+
ownership.tmuxSessionId
|
|
774
|
+
)
|
|
775
|
+
: undefined);
|
|
433
776
|
if (!windowId) return;
|
|
434
777
|
|
|
435
|
-
|
|
778
|
+
const checkFormat = ownership
|
|
779
|
+
? `#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`
|
|
780
|
+
: '#{window_name}';
|
|
781
|
+
const checkResult = runTmuxCommandSync(['display', '-p', '-t', windowId, checkFormat]);
|
|
782
|
+
|
|
783
|
+
if (!checkResult.success) {
|
|
784
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const parts = checkResult.output?.trim().split('\t') ?? [];
|
|
789
|
+
const windowName = parts[0];
|
|
790
|
+
if (windowName !== 'Agents') {
|
|
791
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (ownership) {
|
|
795
|
+
const [, isOpencode, windowServerKey, windowInstanceId] = parts;
|
|
796
|
+
if (
|
|
797
|
+
isOpencode !== '1' ||
|
|
798
|
+
windowServerKey !== ownership.serverKey ||
|
|
799
|
+
windowInstanceId !== ownership.instanceId
|
|
800
|
+
) {
|
|
801
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
436
806
|
runTmuxCommandSync(['kill-window', '-t', windowId]);
|
|
437
|
-
|
|
438
|
-
clearPersistedAgentsWindowId();
|
|
807
|
+
if (cacheKey) setCachedAgentsWindowId(cacheKey, undefined);
|
|
439
808
|
}
|
|
440
809
|
|
|
441
810
|
/**
|
|
442
811
|
* Get the current agents window ID (for testing/debugging)
|
|
443
|
-
* Also checks persisted file for crash recovery
|
|
444
812
|
*/
|
|
445
|
-
export function getAgentsWindowId(): string | undefined {
|
|
446
|
-
|
|
813
|
+
export function getAgentsWindowId(ownership?: OwnershipContext): string | undefined {
|
|
814
|
+
if (ownership) {
|
|
815
|
+
return getCachedAgentsWindowId(ownership);
|
|
816
|
+
}
|
|
817
|
+
// Return first cached window ID (for backwards compatibility in tests)
|
|
818
|
+
const values = agentsWindowIdByKey.values();
|
|
819
|
+
const first = values.next();
|
|
820
|
+
return first.done ? undefined : first.value;
|
|
447
821
|
}
|
|
448
822
|
|
|
449
823
|
/**
|
|
450
|
-
*
|
|
451
|
-
* This is a fallback cleanup method when PID-based cleanup fails.
|
|
452
|
-
*
|
|
453
|
-
* @param serverUrl - The server URL to match (optional, kills all if not provided)
|
|
454
|
-
* @param logger - Optional logging function for debug output
|
|
455
|
-
* @returns Number of processes killed
|
|
824
|
+
* Clean up owned tmux windows/panes using ownership tags.
|
|
456
825
|
*/
|
|
457
|
-
export async function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
): Promise<number> {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const parts = line.trim().split(/\s+/);
|
|
481
|
-
const pid = Number(parts[1]);
|
|
482
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
483
|
-
matchingPids.push(pid);
|
|
826
|
+
export async function cleanupOwnedResources(
|
|
827
|
+
serverKey: string,
|
|
828
|
+
instanceId: string
|
|
829
|
+
): Promise<{ panesClosed: number; windowClosed: boolean }> {
|
|
830
|
+
if (!instanceId) return { panesClosed: 0, windowClosed: false };
|
|
831
|
+
|
|
832
|
+
const windowsResult = await runTmuxCommand([
|
|
833
|
+
'list-windows',
|
|
834
|
+
'-a',
|
|
835
|
+
'-F',
|
|
836
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
837
|
+
]);
|
|
838
|
+
const windowsToClose = new Set<string>();
|
|
839
|
+
const serverWindowIds = new Set<string>();
|
|
840
|
+
|
|
841
|
+
if (windowsResult.success && windowsResult.output) {
|
|
842
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
843
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId] =
|
|
844
|
+
line.split('\t');
|
|
845
|
+
if (!windowId) continue;
|
|
846
|
+
if (windowServerKey === serverKey) {
|
|
847
|
+
serverWindowIds.add(windowId);
|
|
484
848
|
}
|
|
849
|
+
if (windowName !== 'Agents') continue;
|
|
850
|
+
if (isOpencode !== '1') continue;
|
|
851
|
+
if (windowServerKey !== serverKey) continue;
|
|
852
|
+
if (windowInstanceId !== instanceId) continue;
|
|
853
|
+
windowsToClose.add(windowId);
|
|
485
854
|
}
|
|
855
|
+
}
|
|
486
856
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
857
|
+
const panesResult = await runTmuxCommand([
|
|
858
|
+
'list-panes',
|
|
859
|
+
'-a',
|
|
860
|
+
'-F',
|
|
861
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
862
|
+
]);
|
|
491
863
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
log(`Failed to kill PID ${pid}: ${code}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
864
|
+
const panesToClose: Array<{ paneId: string; panePid?: number; windowId: string }> = [];
|
|
865
|
+
if (panesResult.success && panesResult.output) {
|
|
866
|
+
for (const line of panesResult.output.split('\n')) {
|
|
867
|
+
const [paneId, panePidRaw, windowId, paneInstanceId] = line.split('\t');
|
|
868
|
+
if (!paneId) continue;
|
|
869
|
+
if (!windowId || !serverWindowIds.has(windowId)) continue;
|
|
870
|
+
if (paneInstanceId !== instanceId) continue;
|
|
871
|
+
const panePid = Number(panePidRaw);
|
|
872
|
+
panesToClose.push({
|
|
873
|
+
paneId,
|
|
874
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
875
|
+
windowId,
|
|
876
|
+
});
|
|
508
877
|
}
|
|
878
|
+
}
|
|
509
879
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
for (const pid of matchingPids) {
|
|
514
|
-
if (!isProcessAlive(pid)) continue;
|
|
515
|
-
try {
|
|
516
|
-
process.kill(pid, 'SIGKILL');
|
|
517
|
-
log(`Sent SIGKILL to PID ${pid}`);
|
|
518
|
-
} catch {
|
|
519
|
-
// Ignore errors on SIGKILL
|
|
520
|
-
}
|
|
880
|
+
for (const pane of panesToClose) {
|
|
881
|
+
if (pane.panePid) {
|
|
882
|
+
await killProcessByPid(pane.panePid);
|
|
521
883
|
}
|
|
884
|
+
}
|
|
522
885
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return 0;
|
|
886
|
+
let windowClosed = false;
|
|
887
|
+
for (const windowId of windowsToClose) {
|
|
888
|
+
await runTmuxCommand(['kill-window', '-t', windowId]);
|
|
889
|
+
windowClosed = true;
|
|
528
890
|
}
|
|
891
|
+
|
|
892
|
+
let panesClosed = 0;
|
|
893
|
+
for (const pane of panesToClose) {
|
|
894
|
+
if (windowsToClose.has(pane.windowId)) continue;
|
|
895
|
+
await runTmuxCommand(['kill-pane', '-t', pane.paneId]);
|
|
896
|
+
panesClosed += 1;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return { panesClosed, windowClosed };
|
|
529
900
|
}
|
|
530
901
|
|
|
531
902
|
/**
|
|
532
|
-
* Synchronous
|
|
533
|
-
* Uses spawnSync to ensure completion before process exit.
|
|
534
|
-
*
|
|
535
|
-
* @param serverUrl - The server URL to match (optional, kills all if not provided)
|
|
536
|
-
* @param logger - Optional logging function for debug output
|
|
537
|
-
* @returns Number of processes killed
|
|
903
|
+
* Synchronous cleanup for owned tmux windows/panes.
|
|
538
904
|
*/
|
|
539
|
-
export function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
): number {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (!
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
if (line.includes(String(process.pid))) continue;
|
|
562
|
-
|
|
563
|
-
const parts = line.trim().split(/\s+/);
|
|
564
|
-
const pid = Number(parts[1]);
|
|
565
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
566
|
-
matchingPids.push(pid);
|
|
905
|
+
export function cleanupOwnedResourcesSync(
|
|
906
|
+
serverKey: string,
|
|
907
|
+
instanceId: string
|
|
908
|
+
): { panesClosed: number; windowClosed: boolean } {
|
|
909
|
+
if (!instanceId) return { panesClosed: 0, windowClosed: false };
|
|
910
|
+
|
|
911
|
+
const windowsResult = runTmuxCommandSync([
|
|
912
|
+
'list-windows',
|
|
913
|
+
'-a',
|
|
914
|
+
'-F',
|
|
915
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
916
|
+
]);
|
|
917
|
+
const windowsToClose = new Set<string>();
|
|
918
|
+
const serverWindowIds = new Set<string>();
|
|
919
|
+
|
|
920
|
+
if (windowsResult.success && windowsResult.output) {
|
|
921
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
922
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId] =
|
|
923
|
+
line.split('\t');
|
|
924
|
+
if (!windowId) continue;
|
|
925
|
+
if (windowServerKey === serverKey) {
|
|
926
|
+
serverWindowIds.add(windowId);
|
|
567
927
|
}
|
|
928
|
+
if (windowName !== 'Agents') continue;
|
|
929
|
+
if (isOpencode !== '1') continue;
|
|
930
|
+
if (windowServerKey !== serverKey) continue;
|
|
931
|
+
if (windowInstanceId !== instanceId) continue;
|
|
932
|
+
windowsToClose.add(windowId);
|
|
568
933
|
}
|
|
934
|
+
}
|
|
569
935
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
for (const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
936
|
+
const panesResult = runTmuxCommandSync([
|
|
937
|
+
'list-panes',
|
|
938
|
+
'-a',
|
|
939
|
+
'-F',
|
|
940
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
941
|
+
]);
|
|
942
|
+
const panesToClose: Array<{ paneId: string; panePid?: number; windowId: string }> = [];
|
|
943
|
+
|
|
944
|
+
if (panesResult.success && panesResult.output) {
|
|
945
|
+
for (const line of panesResult.output.split('\n')) {
|
|
946
|
+
const [paneId, panePidRaw, windowId, paneInstanceId] = line.split('\t');
|
|
947
|
+
if (!paneId) continue;
|
|
948
|
+
if (!windowId || !serverWindowIds.has(windowId)) continue;
|
|
949
|
+
if (paneInstanceId !== instanceId) continue;
|
|
950
|
+
const panePid = Number(panePidRaw);
|
|
951
|
+
panesToClose.push({
|
|
952
|
+
paneId,
|
|
953
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
954
|
+
windowId,
|
|
955
|
+
});
|
|
590
956
|
}
|
|
957
|
+
}
|
|
591
958
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const view = new Int32Array(buffer);
|
|
596
|
-
Atomics.wait(view, 0, 0, PROCESS_TERM_WAIT_MS);
|
|
597
|
-
} catch {
|
|
598
|
-
// Ignore sleep errors
|
|
959
|
+
for (const pane of panesToClose) {
|
|
960
|
+
if (pane.panePid) {
|
|
961
|
+
killProcessByPidSync(pane.panePid);
|
|
599
962
|
}
|
|
963
|
+
}
|
|
600
964
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
log(`Sent SIGKILL to PID ${pid}`);
|
|
607
|
-
} catch {
|
|
608
|
-
// Ignore errors
|
|
609
|
-
}
|
|
610
|
-
}
|
|
965
|
+
let windowClosed = false;
|
|
966
|
+
for (const windowId of windowsToClose) {
|
|
967
|
+
runTmuxCommandSync(['kill-window', '-t', windowId]);
|
|
968
|
+
windowClosed = true;
|
|
969
|
+
}
|
|
611
970
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
971
|
+
let panesClosed = 0;
|
|
972
|
+
for (const pane of panesToClose) {
|
|
973
|
+
if (windowsToClose.has(pane.windowId)) continue;
|
|
974
|
+
runTmuxCommandSync(['kill-pane', '-t', pane.paneId]);
|
|
975
|
+
panesClosed += 1;
|
|
617
976
|
}
|
|
977
|
+
|
|
978
|
+
return { panesClosed, windowClosed };
|
|
618
979
|
}
|