@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/src/tmux/index.ts
CHANGED
|
@@ -8,7 +8,10 @@ export {
|
|
|
8
8
|
closeAgentsWindow,
|
|
9
9
|
closeAgentsWindowSync,
|
|
10
10
|
getAgentsWindowId,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
cleanupOwnedResources,
|
|
12
|
+
cleanupOwnedResourcesSync,
|
|
13
|
+
findOwnedAgentPanes,
|
|
14
|
+
getPanePid,
|
|
15
|
+
getPanePidSync,
|
|
13
16
|
} from './executor';
|
|
14
17
|
export { TmuxSessionManager } from './manager';
|
package/src/tmux/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'bun';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import type {
|
|
4
5
|
PaneAction,
|
|
5
6
|
TmuxConfig,
|
|
@@ -9,7 +10,13 @@ import type {
|
|
|
9
10
|
SessionMapping,
|
|
10
11
|
} from './types';
|
|
11
12
|
import { POLL_INTERVAL_MS, SESSION_MISSING_GRACE_MS, SESSION_TIMEOUT_MS } from './types';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
canonicalizeServerUrl,
|
|
15
|
+
getCurrentPaneId,
|
|
16
|
+
getTmuxPath,
|
|
17
|
+
getTmuxSessionId,
|
|
18
|
+
isInsideTmux,
|
|
19
|
+
} from './utils';
|
|
13
20
|
import { queryWindowState } from './state-query';
|
|
14
21
|
import { decideSpawnActions } from './decision-engine';
|
|
15
22
|
import {
|
|
@@ -18,8 +25,11 @@ import {
|
|
|
18
25
|
closeAgentsWindowSync,
|
|
19
26
|
closePaneById,
|
|
20
27
|
killProcessByPid,
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
getPanePid,
|
|
29
|
+
getPanePidSync,
|
|
30
|
+
cleanupOwnedResources,
|
|
31
|
+
cleanupOwnedResourcesSync,
|
|
32
|
+
findOwnedAgentPanes,
|
|
23
33
|
} from './executor';
|
|
24
34
|
|
|
25
35
|
/**
|
|
@@ -55,6 +65,16 @@ export class TmuxSessionManager {
|
|
|
55
65
|
private pendingSessions = new Set<string>();
|
|
56
66
|
private pollInterval?: ReturnType<typeof setInterval>;
|
|
57
67
|
private sourcePaneId: string | undefined;
|
|
68
|
+
private tmuxSessionId: string | undefined;
|
|
69
|
+
private instanceId = randomUUID().slice(0, 8);
|
|
70
|
+
private ownerPid = process.pid;
|
|
71
|
+
private serverKey: string | undefined;
|
|
72
|
+
private statusMissingSince = new Map<string, number>();
|
|
73
|
+
/**
|
|
74
|
+
* Operation queue to serialize tmux mutations.
|
|
75
|
+
* This prevents race conditions when multiple sessions are created rapidly.
|
|
76
|
+
*/
|
|
77
|
+
private tmuxOpQueue: Promise<void> = Promise.resolve();
|
|
58
78
|
|
|
59
79
|
constructor(
|
|
60
80
|
private ctx: PluginInput,
|
|
@@ -64,6 +84,20 @@ export class TmuxSessionManager {
|
|
|
64
84
|
this.sourcePaneId = getCurrentPaneId();
|
|
65
85
|
}
|
|
66
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Enqueue a tmux operation to ensure sequential execution.
|
|
89
|
+
* This prevents race conditions when multiple sessions are created/deleted rapidly.
|
|
90
|
+
*/
|
|
91
|
+
private enqueue<T>(fn: () => Promise<T>): Promise<T> {
|
|
92
|
+
const result = this.tmuxOpQueue.then(fn, fn); // Run even if previous failed
|
|
93
|
+
// Update queue but don't propagate errors to next operation
|
|
94
|
+
this.tmuxOpQueue = result.then(
|
|
95
|
+
() => {},
|
|
96
|
+
() => {}
|
|
97
|
+
);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
67
101
|
/**
|
|
68
102
|
* Check if tmux integration is enabled and available
|
|
69
103
|
*/
|
|
@@ -74,11 +108,25 @@ export class TmuxSessionManager {
|
|
|
74
108
|
/**
|
|
75
109
|
* Handle a new background session being created
|
|
76
110
|
* This is called by BackgroundManager when a background task starts
|
|
111
|
+
*
|
|
112
|
+
* Operations are queued to prevent race conditions when multiple sessions
|
|
113
|
+
* are created rapidly.
|
|
77
114
|
*/
|
|
78
115
|
async onSessionCreated(event: {
|
|
79
116
|
sessionId: string;
|
|
80
117
|
parentId: string;
|
|
81
118
|
title: string;
|
|
119
|
+
}): Promise<void> {
|
|
120
|
+
return this.enqueue(() => this.doSessionCreated(event));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Internal implementation of session creation (runs within the queue)
|
|
125
|
+
*/
|
|
126
|
+
private async doSessionCreated(event: {
|
|
127
|
+
sessionId: string;
|
|
128
|
+
parentId: string;
|
|
129
|
+
title: string;
|
|
82
130
|
}): Promise<void> {
|
|
83
131
|
this.log(`onSessionCreated called for ${event.sessionId} (${event.title})`);
|
|
84
132
|
|
|
@@ -110,6 +158,14 @@ export class TmuxSessionManager {
|
|
|
110
158
|
return;
|
|
111
159
|
}
|
|
112
160
|
|
|
161
|
+
// Get the tmux session ID for this pane (cached after first lookup)
|
|
162
|
+
if (!this.tmuxSessionId) {
|
|
163
|
+
this.tmuxSessionId = await getTmuxSessionId(this.sourcePaneId);
|
|
164
|
+
if (this.tmuxSessionId) {
|
|
165
|
+
this.log(`Resolved tmux session ID: ${this.tmuxSessionId}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
113
169
|
const state = await queryWindowState(this.sourcePaneId);
|
|
114
170
|
if (!state) {
|
|
115
171
|
this.log('Failed to query tmux window state.');
|
|
@@ -150,10 +206,19 @@ export class TmuxSessionManager {
|
|
|
150
206
|
return;
|
|
151
207
|
}
|
|
152
208
|
|
|
209
|
+
const serverKey = canonicalizeServerUrl(serverUrl);
|
|
210
|
+
this.serverKey = serverKey;
|
|
211
|
+
|
|
153
212
|
const result = await executeActions(decision.actions, {
|
|
154
213
|
config: this.config,
|
|
155
214
|
serverUrl,
|
|
156
215
|
windowState: state,
|
|
216
|
+
ownership: {
|
|
217
|
+
instanceId: this.instanceId,
|
|
218
|
+
ownerPid: this.ownerPid,
|
|
219
|
+
serverKey,
|
|
220
|
+
tmuxSessionId: this.tmuxSessionId,
|
|
221
|
+
},
|
|
157
222
|
});
|
|
158
223
|
|
|
159
224
|
if (!result.success) {
|
|
@@ -162,6 +227,7 @@ export class TmuxSessionManager {
|
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
this.applyActionResults(decision.actions, result.results);
|
|
230
|
+
await this.refreshMissingPids();
|
|
165
231
|
this.log(
|
|
166
232
|
`Successfully spawned pane for ${event.sessionId}. Tracking ${this.sessions.size} sessions. PIDs: ${this.getTrackedPids().join(', ') || 'none'}`
|
|
167
233
|
);
|
|
@@ -188,8 +254,17 @@ export class TmuxSessionManager {
|
|
|
188
254
|
* Explicitly kills the pane when a background session completes.
|
|
189
255
|
* We can't rely on `opencode attach` exiting because it's an interactive
|
|
190
256
|
* terminal that keeps running even after the session goes idle.
|
|
257
|
+
*
|
|
258
|
+
* Operations are queued to prevent race conditions.
|
|
191
259
|
*/
|
|
192
260
|
async onSessionDeleted(event: { sessionId: string }): Promise<void> {
|
|
261
|
+
return this.enqueue(() => this.doSessionDeleted(event));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Internal implementation of session deletion (runs within the queue)
|
|
266
|
+
*/
|
|
267
|
+
private async doSessionDeleted(event: { sessionId: string }): Promise<void> {
|
|
193
268
|
this.log(`onSessionDeleted called for ${event.sessionId}`);
|
|
194
269
|
|
|
195
270
|
if (!this.isEnabled()) {
|
|
@@ -218,6 +293,7 @@ export class TmuxSessionManager {
|
|
|
218
293
|
|
|
219
294
|
// Update internal state
|
|
220
295
|
this.sessions.delete(event.sessionId);
|
|
296
|
+
this.statusMissingSince.delete(event.sessionId);
|
|
221
297
|
this.log(`Removed session from tracking. Now tracking ${this.sessions.size} sessions.`);
|
|
222
298
|
|
|
223
299
|
if (this.sessions.size === 0) {
|
|
@@ -235,27 +311,30 @@ export class TmuxSessionManager {
|
|
|
235
311
|
this.log('Starting cleanup...');
|
|
236
312
|
this.stopPolling();
|
|
237
313
|
|
|
238
|
-
let pidCleanupFailed = false;
|
|
239
314
|
for (const session of this.sessions.values()) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
315
|
+
let pid = session.pid;
|
|
316
|
+
if (!pid) {
|
|
317
|
+
pid = await getPanePid(session.paneId);
|
|
318
|
+
if (pid) {
|
|
319
|
+
session.pid = pid;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!pid) continue;
|
|
323
|
+
this.log(`Killing process ${pid} for session ${session.sessionId}`);
|
|
324
|
+
const success = await killProcessByPid(pid);
|
|
243
325
|
if (!success) {
|
|
244
|
-
this.log(`Failed to kill process ${
|
|
245
|
-
pidCleanupFailed = true;
|
|
326
|
+
this.log(`Failed to kill process ${pid} for session ${session.sessionId}`);
|
|
246
327
|
}
|
|
247
328
|
}
|
|
248
329
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (pidCleanupFailed) {
|
|
255
|
-
this.log('PID-based cleanup had failures, running fallback cleanup...');
|
|
256
|
-
const serverUrl = this.getServerUrl();
|
|
257
|
-
await killOrphanedAttachProcesses(serverUrl, (msg) => this.log(msg));
|
|
330
|
+
const serverKey = this.getServerKey();
|
|
331
|
+
if (serverKey) {
|
|
332
|
+
await cleanupOwnedResources(serverKey, this.instanceId);
|
|
333
|
+
} else {
|
|
334
|
+
await closeAgentsWindow();
|
|
258
335
|
}
|
|
336
|
+
this.sessions.clear();
|
|
337
|
+
this.statusMissingSince.clear();
|
|
259
338
|
|
|
260
339
|
this.log('Cleanup complete');
|
|
261
340
|
}
|
|
@@ -270,30 +349,27 @@ export class TmuxSessionManager {
|
|
|
270
349
|
this.log('Starting sync cleanup...');
|
|
271
350
|
this.stopPolling();
|
|
272
351
|
|
|
273
|
-
let pidCleanupFailed = false;
|
|
274
352
|
for (const session of this.sessions.values()) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
pidCleanupFailed = true; // Process still exists
|
|
282
|
-
} catch {
|
|
283
|
-
// Process is dead, good
|
|
353
|
+
let pid = session.pid;
|
|
354
|
+
if (!pid) {
|
|
355
|
+
pid = getPanePidSync(session.paneId);
|
|
356
|
+
if (pid) {
|
|
357
|
+
session.pid = pid;
|
|
358
|
+
}
|
|
284
359
|
}
|
|
360
|
+
if (!pid) continue;
|
|
361
|
+
this.log(`Killing process ${pid} for session ${session.sessionId}`);
|
|
362
|
+
this.killProcessByPidSync(pid);
|
|
285
363
|
}
|
|
286
364
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (pidCleanupFailed) {
|
|
293
|
-
this.log('PID-based cleanup had failures, running fallback cleanup...');
|
|
294
|
-
const serverUrl = this.getServerUrl();
|
|
295
|
-
killOrphanedAttachProcessesSync(serverUrl, (msg) => this.log(msg));
|
|
365
|
+
const serverKey = this.getServerKey();
|
|
366
|
+
if (serverKey) {
|
|
367
|
+
cleanupOwnedResourcesSync(serverKey, this.instanceId);
|
|
368
|
+
} else {
|
|
369
|
+
closeAgentsWindowSync();
|
|
296
370
|
}
|
|
371
|
+
this.sessions.clear();
|
|
372
|
+
this.statusMissingSince.clear();
|
|
297
373
|
|
|
298
374
|
this.log('Sync cleanup complete');
|
|
299
375
|
}
|
|
@@ -321,30 +397,73 @@ export class TmuxSessionManager {
|
|
|
321
397
|
* Poll active sessions for status changes
|
|
322
398
|
*/
|
|
323
399
|
private async pollSessions(): Promise<void> {
|
|
400
|
+
return this.enqueue(() => this.doPollSessions());
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Poll active sessions for status changes
|
|
405
|
+
*/
|
|
406
|
+
private async doPollSessions(): Promise<void> {
|
|
324
407
|
if (!this.isEnabled()) return;
|
|
325
408
|
if (!this.sourcePaneId) return;
|
|
326
409
|
|
|
327
410
|
const state = await queryWindowState(this.sourcePaneId);
|
|
328
411
|
if (!state) return;
|
|
412
|
+
const statusMap = await this.fetchSessionStatuses();
|
|
329
413
|
|
|
330
414
|
const now = Date.now();
|
|
415
|
+
const sessionsToClose: TrackedSession[] = [];
|
|
331
416
|
for (const session of this.sessions.values()) {
|
|
332
417
|
const pane = findPane(state, session.paneId);
|
|
333
418
|
if (pane) {
|
|
334
419
|
session.lastSeenAt = new Date();
|
|
335
|
-
|
|
420
|
+
if (!session.pid) {
|
|
421
|
+
const pid = await getPanePid(session.paneId);
|
|
422
|
+
if (pid) {
|
|
423
|
+
session.pid = pid;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const missingFor = now - session.lastSeenAt.getTime();
|
|
428
|
+
if (missingFor > SESSION_MISSING_GRACE_MS) {
|
|
429
|
+
if (session.pid) {
|
|
430
|
+
await killProcessByPid(session.pid);
|
|
431
|
+
}
|
|
432
|
+
this.sessions.delete(session.sessionId);
|
|
433
|
+
this.statusMissingSince.delete(session.sessionId);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
336
436
|
}
|
|
337
437
|
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
this.
|
|
341
|
-
|
|
438
|
+
const status = statusMap[session.sessionId];
|
|
439
|
+
if (status) {
|
|
440
|
+
this.statusMissingSince.delete(session.sessionId);
|
|
441
|
+
} else if (!this.statusMissingSince.has(session.sessionId)) {
|
|
442
|
+
this.statusMissingSince.set(session.sessionId, now);
|
|
342
443
|
}
|
|
343
444
|
|
|
445
|
+
const statusMissingAt = this.statusMissingSince.get(session.sessionId);
|
|
446
|
+
const missingTooLong =
|
|
447
|
+
statusMissingAt !== undefined && now - statusMissingAt > SESSION_MISSING_GRACE_MS;
|
|
344
448
|
const age = now - session.createdAt.getTime();
|
|
345
|
-
|
|
346
|
-
|
|
449
|
+
const isTimedOut = age > SESSION_TIMEOUT_MS;
|
|
450
|
+
const isIdle = status?.type === 'idle';
|
|
451
|
+
|
|
452
|
+
if (isIdle || missingTooLong || isTimedOut) {
|
|
453
|
+
sessionsToClose.push(session);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const session of sessionsToClose) {
|
|
458
|
+
this.log(`Closing idle session ${session.sessionId} (pane: ${session.paneId})`);
|
|
459
|
+
const result = await closePaneById(session.paneId, session.pid);
|
|
460
|
+
if (!result.success) {
|
|
461
|
+
this.log(
|
|
462
|
+
`Failed to close pane ${session.paneId} for session ${session.sessionId}: ${result.error}`
|
|
463
|
+
);
|
|
347
464
|
}
|
|
465
|
+
this.sessions.delete(session.sessionId);
|
|
466
|
+
this.statusMissingSince.delete(session.sessionId);
|
|
348
467
|
}
|
|
349
468
|
|
|
350
469
|
if (this.sessions.size === 0) {
|
|
@@ -374,6 +493,20 @@ export class TmuxSessionManager {
|
|
|
374
493
|
return typeof serverUrl === 'string' ? serverUrl : serverUrl.toString();
|
|
375
494
|
}
|
|
376
495
|
|
|
496
|
+
private getServerKey(): string | undefined {
|
|
497
|
+
if (this.serverKey) return this.serverKey;
|
|
498
|
+
const serverUrl = this.getServerUrl();
|
|
499
|
+
if (!serverUrl) return undefined;
|
|
500
|
+
try {
|
|
501
|
+
const serverKey = canonicalizeServerUrl(serverUrl);
|
|
502
|
+
this.serverKey = serverKey;
|
|
503
|
+
return serverKey;
|
|
504
|
+
} catch (error) {
|
|
505
|
+
this.log(`Failed to canonicalize server URL "${serverUrl}": ${error}`);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
377
510
|
private applyActionResults(
|
|
378
511
|
actions: PaneAction[],
|
|
379
512
|
results: Array<{ action: PaneAction; result: { paneId?: string; pid?: number } }>
|
|
@@ -387,6 +520,7 @@ export class TmuxSessionManager {
|
|
|
387
520
|
break;
|
|
388
521
|
case 'replace':
|
|
389
522
|
this.sessions.delete(action.oldSessionId);
|
|
523
|
+
this.statusMissingSince.delete(action.oldSessionId);
|
|
390
524
|
this.sessions.set(action.newSessionId, {
|
|
391
525
|
sessionId: action.newSessionId,
|
|
392
526
|
paneId: action.paneId,
|
|
@@ -413,6 +547,30 @@ export class TmuxSessionManager {
|
|
|
413
547
|
}
|
|
414
548
|
}
|
|
415
549
|
|
|
550
|
+
private async refreshMissingPids(): Promise<void> {
|
|
551
|
+
for (const session of this.sessions.values()) {
|
|
552
|
+
if (session.pid) continue;
|
|
553
|
+
const pid = await getPanePid(session.paneId);
|
|
554
|
+
if (pid) {
|
|
555
|
+
session.pid = pid;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private async fetchSessionStatuses(): Promise<Record<string, { type?: string }>> {
|
|
561
|
+
try {
|
|
562
|
+
const result = await this.ctx.client.session.status({
|
|
563
|
+
path: undefined,
|
|
564
|
+
throwOnError: false,
|
|
565
|
+
});
|
|
566
|
+
const data = unwrapResponse<Record<string, { type?: string }>>(result);
|
|
567
|
+
if (!data || typeof data !== 'object') return {};
|
|
568
|
+
return data;
|
|
569
|
+
} catch {
|
|
570
|
+
return {};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
416
574
|
/**
|
|
417
575
|
* Find and report orphaned processes (does NOT kill them by default).
|
|
418
576
|
* Call this manually if you need to identify orphaned processes after a crash.
|
|
@@ -424,56 +582,31 @@ export class TmuxSessionManager {
|
|
|
424
582
|
*/
|
|
425
583
|
async reportOrphanedProcesses(): Promise<number[]> {
|
|
426
584
|
if (!this.isEnabled()) return [];
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
585
|
+
const serverKey = this.getServerKey();
|
|
586
|
+
if (!serverKey) return [];
|
|
429
587
|
|
|
430
588
|
const trackedSessionIds = new Set(this.sessions.keys());
|
|
431
|
-
const
|
|
589
|
+
const panes = await findOwnedAgentPanes(serverKey);
|
|
590
|
+
const orphanedPanes = panes.filter((pane) => {
|
|
591
|
+
if (!pane.sessionId) return false;
|
|
592
|
+
return !trackedSessionIds.has(pane.sessionId);
|
|
593
|
+
});
|
|
432
594
|
|
|
433
|
-
if (
|
|
595
|
+
if (orphanedPanes.length > 0) {
|
|
596
|
+
const sessionIds = orphanedPanes
|
|
597
|
+
.map((pane) => pane.sessionId)
|
|
598
|
+
.filter((sessionId): sessionId is string => !!sessionId);
|
|
434
599
|
this.log(
|
|
435
|
-
`Found ${
|
|
600
|
+
`Found ${orphanedPanes.length} potentially orphaned sessions: ${sessionIds.join(', ')}`
|
|
436
601
|
);
|
|
437
602
|
this.log(
|
|
438
|
-
'These may be user-initiated sessions.
|
|
603
|
+
'These may be user-initiated sessions. Close their tmux panes manually if needed.'
|
|
439
604
|
);
|
|
440
605
|
}
|
|
441
606
|
|
|
442
|
-
return
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
private async findOrphanedAttachPids(
|
|
446
|
-
serverUrl: string,
|
|
447
|
-
trackedSessionIds: Set<string>
|
|
448
|
-
): Promise<number[]> {
|
|
449
|
-
try {
|
|
450
|
-
const proc = spawn(['ps', 'aux'], { stdout: 'pipe', stderr: 'pipe' });
|
|
451
|
-
await proc.exited;
|
|
452
|
-
const output = await new Response(proc.stdout).text();
|
|
453
|
-
const lines = output.split('\n');
|
|
454
|
-
const matches: number[] = [];
|
|
455
|
-
|
|
456
|
-
for (const line of lines) {
|
|
457
|
-
if (!line.includes('opencode attach')) continue;
|
|
458
|
-
if (!line.includes(serverUrl)) continue;
|
|
459
|
-
const parts = line.trim().split(/\s+/);
|
|
460
|
-
const pid = Number(parts[1]);
|
|
461
|
-
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
462
|
-
if (pid === process.pid) continue;
|
|
463
|
-
const sessionId = this.extractSessionId(line);
|
|
464
|
-
if (sessionId && trackedSessionIds.has(sessionId)) continue;
|
|
465
|
-
matches.push(pid);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return matches;
|
|
469
|
-
} catch {
|
|
470
|
-
return [];
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private extractSessionId(line: string): string | undefined {
|
|
475
|
-
const match = line.match(/--session\s+(['"]?)([^'";\s]+)\1/);
|
|
476
|
-
return match?.[2];
|
|
607
|
+
return orphanedPanes
|
|
608
|
+
.map((pane) => pane.panePid)
|
|
609
|
+
.filter((pid): pid is number => typeof pid === 'number');
|
|
477
610
|
}
|
|
478
611
|
|
|
479
612
|
/**
|
|
@@ -543,31 +676,34 @@ export class TmuxSessionManager {
|
|
|
543
676
|
*
|
|
544
677
|
* @param serverUrl - Optional server URL to filter processes
|
|
545
678
|
* @param logger - Optional logging function
|
|
679
|
+
* @param instanceId - Ownership instance id to target cleanup
|
|
546
680
|
* @returns Object with cleanup results
|
|
547
681
|
*/
|
|
548
682
|
static async cleanupOrphans(
|
|
549
683
|
serverUrl?: string,
|
|
550
|
-
logger?: (msg: string) => void
|
|
684
|
+
logger?: (msg: string) => void,
|
|
685
|
+
instanceId?: string
|
|
551
686
|
): Promise<{ killed: number; windowClosed: boolean }> {
|
|
552
687
|
const log = logger ?? (() => {});
|
|
553
688
|
|
|
554
689
|
log('Starting orphan cleanup...');
|
|
555
690
|
|
|
556
|
-
|
|
557
|
-
let windowClosed = false;
|
|
691
|
+
let serverKey: string | undefined;
|
|
558
692
|
try {
|
|
559
|
-
|
|
560
|
-
windowClosed = true;
|
|
561
|
-
log('Closed agents window');
|
|
693
|
+
serverKey = serverUrl ? canonicalizeServerUrl(serverUrl) : undefined;
|
|
562
694
|
} catch {
|
|
563
|
-
|
|
695
|
+
serverKey = undefined;
|
|
696
|
+
}
|
|
697
|
+
if (!serverKey || !instanceId) {
|
|
698
|
+
log('No server URL or instance ID provided; skipping ownership cleanup.');
|
|
699
|
+
return { killed: 0, windowClosed: false };
|
|
564
700
|
}
|
|
565
701
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return { killed, windowClosed };
|
|
702
|
+
const result = await cleanupOwnedResources(serverKey, instanceId);
|
|
703
|
+
log(
|
|
704
|
+
`Orphan cleanup complete: ${result.panesClosed} panes closed, window closed: ${result.windowClosed}`
|
|
705
|
+
);
|
|
706
|
+
return { killed: result.panesClosed, windowClosed: result.windowClosed };
|
|
571
707
|
}
|
|
572
708
|
}
|
|
573
709
|
|
|
@@ -575,3 +711,10 @@ function findPane(state: WindowState, paneId: string): TmuxPaneInfo | undefined
|
|
|
575
711
|
if (state.mainPane?.paneId === paneId) return state.mainPane;
|
|
576
712
|
return state.agentPanes.find((pane) => pane.paneId === paneId);
|
|
577
713
|
}
|
|
714
|
+
|
|
715
|
+
function unwrapResponse<T>(result: unknown): T | undefined {
|
|
716
|
+
if (typeof result === 'object' && result !== null && 'data' in result) {
|
|
717
|
+
return (result as { data?: T }).data;
|
|
718
|
+
}
|
|
719
|
+
return result as T;
|
|
720
|
+
}
|
package/src/tmux/types.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface TmuxPaneInfo {
|
|
|
11
11
|
isActive: boolean; // Is this the active pane?
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Ownership tags stored in tmux user options
|
|
16
|
+
*/
|
|
17
|
+
export interface TmuxOwnershipTags {
|
|
18
|
+
isOpencode: boolean; // @agentuity.opencode = "1"
|
|
19
|
+
serverKey: string; // @agentuity.opencode.server
|
|
20
|
+
ownerPid: number; // @agentuity.opencode.ownerPid
|
|
21
|
+
instanceId: string; // @agentuity.opencode.instance
|
|
22
|
+
sessionId?: string; // @agentuity.opencode.session (pane-level)
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Current state of the tmux window
|
|
16
27
|
*/
|
package/src/tmux/utils.ts
CHANGED
|
@@ -83,3 +83,42 @@ export function getTmuxPathSync(): string | null {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the tmux session ID for a given pane.
|
|
89
|
+
* This is used to ensure windows are created in the correct tmux session
|
|
90
|
+
* when multiple opencode instances run in different sessions.
|
|
91
|
+
*/
|
|
92
|
+
export async function getTmuxSessionId(paneId: string): Promise<string | undefined> {
|
|
93
|
+
const result = await runTmuxCommand(['display', '-p', '-t', paneId, '#{session_id}']);
|
|
94
|
+
if (!result.success || !result.output) return undefined;
|
|
95
|
+
return result.output.trim() || undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the tmux session ID synchronously (for shutdown scenarios)
|
|
100
|
+
*/
|
|
101
|
+
export function getTmuxSessionIdSync(paneId: string): string | undefined {
|
|
102
|
+
const result = runTmuxCommandSync(['display', '-p', '-t', paneId, '#{session_id}']);
|
|
103
|
+
if (!result.success || !result.output) return undefined;
|
|
104
|
+
return result.output.trim() || undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Canonicalize server URL for consistent ownership tagging.
|
|
109
|
+
* Normalizes loopback addresses (127.0.0.1, ::1) to localhost.
|
|
110
|
+
*
|
|
111
|
+
* @throws Error if URL is invalid
|
|
112
|
+
*/
|
|
113
|
+
export function canonicalizeServerUrl(url: string): string {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = new URL(url);
|
|
116
|
+
let host = parsed.hostname.toLowerCase();
|
|
117
|
+
if (host === '127.0.0.1' || host === '::1') {
|
|
118
|
+
host = 'localhost';
|
|
119
|
+
}
|
|
120
|
+
return `${parsed.protocol}//${host}:${parsed.port}`;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(`Invalid server URL: ${url}`, { cause: error });
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/tools/background.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function createBackgroundTools(manager: BackgroundManager): {
|
|
|
60
60
|
};
|
|
61
61
|
} {
|
|
62
62
|
const backgroundTaskTool = {
|
|
63
|
-
name: '
|
|
63
|
+
name: 'agentuity_background_task',
|
|
64
64
|
description: 'Launch a task to run in the background.',
|
|
65
65
|
args: BackgroundTaskArgsSchema,
|
|
66
66
|
async execute(
|
|
@@ -92,7 +92,7 @@ export function createBackgroundTools(manager: BackgroundManager): {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const backgroundOutputTool = {
|
|
95
|
-
name: '
|
|
95
|
+
name: 'agentuity_background_output',
|
|
96
96
|
description: 'Retrieve output for a background task.',
|
|
97
97
|
args: BackgroundOutputArgsSchema,
|
|
98
98
|
async execute(args: BackgroundOutputArgs): Promise<{
|
|
@@ -119,7 +119,7 @@ export function createBackgroundTools(manager: BackgroundManager): {
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
const backgroundCancelTool = {
|
|
122
|
-
name: '
|
|
122
|
+
name: 'agentuity_background_cancel',
|
|
123
123
|
description: 'Cancel a running background task.',
|
|
124
124
|
args: BackgroundCancelArgsSchema,
|
|
125
125
|
async execute(args: BackgroundCancelArgs): Promise<{
|
package/src/tools/delegate.ts
CHANGED
|
@@ -4,7 +4,18 @@ import type { AgentRole } from '../types';
|
|
|
4
4
|
// Schema for the delegate tool
|
|
5
5
|
export const DelegateArgsSchema = z.object({
|
|
6
6
|
agent: z
|
|
7
|
-
.enum([
|
|
7
|
+
.enum([
|
|
8
|
+
'scout',
|
|
9
|
+
'builder',
|
|
10
|
+
'architect',
|
|
11
|
+
'reviewer',
|
|
12
|
+
'memory',
|
|
13
|
+
'reasoner',
|
|
14
|
+
'expert',
|
|
15
|
+
'planner',
|
|
16
|
+
'runner',
|
|
17
|
+
'product',
|
|
18
|
+
])
|
|
8
19
|
.describe('The agent to delegate to'),
|
|
9
20
|
task: z.string().describe('Clear description of the task to delegate'),
|
|
10
21
|
context: z.string().optional().describe('Additional context from previous tasks'),
|
|
@@ -27,21 +38,25 @@ const AGENT_MENTIONS: Record<AgentRole, string> = {
|
|
|
27
38
|
expert: '@Agentuity Coder Expert',
|
|
28
39
|
planner: '@Agentuity Coder Planner',
|
|
29
40
|
runner: '@Agentuity Coder Runner',
|
|
41
|
+
reasoner: '@Agentuity Coder Reasoner',
|
|
42
|
+
product: '@Agentuity Coder Product',
|
|
30
43
|
};
|
|
31
44
|
|
|
32
45
|
export const delegateTool = {
|
|
33
|
-
name: '
|
|
46
|
+
name: 'agentuity_coder_delegate',
|
|
34
47
|
description: `Delegate a task to a specialized Agentuity Coder agent.
|
|
35
48
|
|
|
36
49
|
Use this to:
|
|
37
50
|
- Scout: Explore codebase, find patterns, research documentation
|
|
38
51
|
- Builder: Implement features, write code, run tests (interactive work)
|
|
39
52
|
- Architect: Complex autonomous tasks, Cadence mode, deep reasoning (GPT Codex)
|
|
40
|
-
- Reviewer: Review changes, catch issues, apply fixes
|
|
53
|
+
- Reviewer: Review changes, catch issues, apply fixes
|
|
41
54
|
- Memory: Store context, remember decisions across sessions
|
|
55
|
+
- Reasoner: Extract structured conclusions, resolve conflicts, surface corrections
|
|
42
56
|
- Expert: Get help with Agentuity CLI and cloud services
|
|
43
57
|
- Planner: Strategic advisor for complex architecture and deep planning (read-only)
|
|
44
58
|
- Runner: Run lint/build/test/typecheck/format/clean/install commands, returns structured results
|
|
59
|
+
- Product: Drive clarity on requirements, validate features, track progress, Cadence briefings
|
|
45
60
|
|
|
46
61
|
The task will be executed by the specified agent and the result returned.`,
|
|
47
62
|
|