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