@agentmeshhq/agent 0.1.13 → 0.1.14
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/__tests__/loader.test.js +44 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/sandbox.test.js.map +1 -1
- package/dist/__tests__/watchdog.test.d.ts +1 -0
- package/dist/__tests__/watchdog.test.js +290 -0
- package/dist/__tests__/watchdog.test.js.map +1 -0
- package/dist/cli/attach.js +20 -1
- package/dist/cli/attach.js.map +1 -1
- package/dist/cli/build.js +8 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/deploy.js +1 -1
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/list.js +3 -3
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/local.js +5 -3
- package/dist/cli/local.js.map +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/nudge.js +16 -3
- package/dist/cli/nudge.js.map +1 -1
- package/dist/cli/restart.js.map +1 -1
- package/dist/cli/slack.js +1 -1
- package/dist/cli/slack.js.map +1 -1
- package/dist/cli/stop.js +13 -5
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/test.js +1 -1
- package/dist/cli/test.js.map +1 -1
- package/dist/cli/token.js +2 -2
- package/dist/cli/token.js.map +1 -1
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/loader.js +27 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +13 -0
- package/dist/core/daemon.d.ts +32 -1
- package/dist/core/daemon.js +362 -19
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.d.ts +2 -2
- package/dist/core/injector.js +23 -4
- package/dist/core/injector.js.map +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/runner.js +23 -1
- package/dist/core/runner.js.map +1 -1
- package/dist/core/sandbox.d.ts +11 -0
- package/dist/core/sandbox.js +34 -2
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/tmux.d.ts +8 -0
- package/dist/core/tmux.js +28 -1
- package/dist/core/tmux.js.map +1 -1
- package/dist/core/watchdog.d.ts +41 -0
- package/dist/core/watchdog.js +198 -0
- package/dist/core/watchdog.js.map +1 -0
- package/dist/core/websocket.js +1 -1
- package/dist/core/websocket.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/loader.test.ts +52 -4
- package/src/__tests__/runner.test.ts +1 -2
- package/src/__tests__/sandbox.test.ts +1 -1
- package/src/__tests__/watchdog.test.ts +368 -0
- package/src/cli/attach.ts +22 -1
- package/src/cli/build.ts +12 -4
- package/src/cli/context.ts +0 -1
- package/src/cli/deploy.ts +7 -5
- package/src/cli/init.ts +7 -19
- package/src/cli/list.ts +6 -10
- package/src/cli/local.ts +21 -12
- package/src/cli/migrate.ts +6 -4
- package/src/cli/nudge.ts +29 -14
- package/src/cli/restart.ts +1 -1
- package/src/cli/slack.ts +16 -15
- package/src/cli/stop.ts +14 -5
- package/src/cli/test.ts +5 -3
- package/src/cli/token.ts +4 -4
- package/src/config/loader.ts +29 -2
- package/src/config/schema.ts +14 -0
- package/src/core/daemon.ts +439 -24
- package/src/core/injector.ts +27 -4
- package/src/core/runner.ts +26 -1
- package/src/core/sandbox.ts +47 -2
- package/src/core/tmux.ts +35 -2
- package/src/core/watchdog.ts +238 -0
- package/src/core/websocket.ts +2 -2
- package/src/index.ts +6 -5
package/src/core/sandbox.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface SandboxConfig {
|
|
|
34
34
|
serveMode?: boolean;
|
|
35
35
|
/** Port for serve mode */
|
|
36
36
|
servePort?: number;
|
|
37
|
+
/** Custom command to run in container (overrides default) */
|
|
38
|
+
command?: string[];
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface SandboxMountPolicy {
|
|
@@ -223,8 +225,11 @@ export class DockerSandbox {
|
|
|
223
225
|
// Image and command
|
|
224
226
|
args.push(this.config.image);
|
|
225
227
|
|
|
226
|
-
// Command:
|
|
227
|
-
if (this.config.
|
|
228
|
+
// Command: custom command > serve mode > tail
|
|
229
|
+
if (this.config.command && this.config.command.length > 0) {
|
|
230
|
+
// Custom command (e.g., agentmesh start inside container)
|
|
231
|
+
args.push(...this.config.command);
|
|
232
|
+
} else if (this.config.serveMode) {
|
|
228
233
|
args.push(
|
|
229
234
|
"opencode",
|
|
230
235
|
"serve",
|
|
@@ -433,6 +438,46 @@ export class DockerSandbox {
|
|
|
433
438
|
this.containerId = null;
|
|
434
439
|
}
|
|
435
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Restarts the container (destroy + start)
|
|
443
|
+
* Returns the new container ID
|
|
444
|
+
*/
|
|
445
|
+
async restart(): Promise<string> {
|
|
446
|
+
await this.destroy();
|
|
447
|
+
|
|
448
|
+
// Generate a new container name
|
|
449
|
+
const suffix = crypto.randomBytes(4).toString("hex");
|
|
450
|
+
this.containerName = `agentmesh-sandbox-${this.config.agentName}-${suffix}`;
|
|
451
|
+
|
|
452
|
+
return this.start();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Force removes any existing containers for this agent
|
|
457
|
+
*/
|
|
458
|
+
static forceCleanup(agentName: string): number {
|
|
459
|
+
const result = spawnSync(
|
|
460
|
+
"docker",
|
|
461
|
+
["ps", "-aq", "--filter", `name=agentmesh-sandbox-${agentName}-`],
|
|
462
|
+
{ encoding: "utf-8" },
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const containers = result.stdout.trim().split("\n").filter(Boolean);
|
|
470
|
+
|
|
471
|
+
for (const containerId of containers) {
|
|
472
|
+
spawnSync("docker", ["rm", "-f", containerId], {
|
|
473
|
+
encoding: "utf-8",
|
|
474
|
+
timeout: 10000,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return containers.length;
|
|
479
|
+
}
|
|
480
|
+
|
|
436
481
|
/**
|
|
437
482
|
* Gets container name
|
|
438
483
|
*/
|
package/src/core/tmux.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
const SESSION_PREFIX = "agentmesh-";
|
|
4
4
|
|
|
@@ -50,7 +50,7 @@ export function createSession(
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
if (envParts.length > 0) {
|
|
53
|
-
envPrefix = envParts.join(" ")
|
|
53
|
+
envPrefix = `${envParts.join(" ")} `;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -194,6 +194,39 @@ export function getSessionInfo(agentName: string): { exists: boolean; command?:
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Checks if the session process is healthy (not crashed/exited)
|
|
199
|
+
* Returns true if healthy, false if crashed or dead
|
|
200
|
+
*/
|
|
201
|
+
export function isSessionHealthy(agentName: string): { healthy: boolean; reason?: string } {
|
|
202
|
+
const sessionName = getSessionName(agentName);
|
|
203
|
+
|
|
204
|
+
if (!sessionExists(sessionName)) {
|
|
205
|
+
return { healthy: false, reason: "session_not_found" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Check if pane is still alive and has a running process
|
|
210
|
+
const paneInfo = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_pid}:#{pane_dead}"`, {
|
|
211
|
+
encoding: "utf-8",
|
|
212
|
+
}).trim();
|
|
213
|
+
|
|
214
|
+
const [pid, dead] = paneInfo.split(":");
|
|
215
|
+
|
|
216
|
+
if (dead === "1") {
|
|
217
|
+
return { healthy: false, reason: "pane_dead" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!pid || pid === "0") {
|
|
221
|
+
return { healthy: false, reason: "no_pid" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { healthy: true };
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return { healthy: false, reason: `check_failed: ${error}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
197
230
|
export interface SessionContext {
|
|
198
231
|
workdir: string;
|
|
199
232
|
gitBranch?: string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Progress Watchdog
|
|
3
|
+
*
|
|
4
|
+
* Monitors agent activity and detects stuck agents.
|
|
5
|
+
* Checks OpenCode logs for activity and tmux pane for permission prompts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
9
|
+
import { captureSessionOutput } from "./tmux.js";
|
|
10
|
+
|
|
11
|
+
export type WatchdogStatus = "active" | "idle" | "stuck" | "permission_blocked";
|
|
12
|
+
|
|
13
|
+
export interface WatchdogResult {
|
|
14
|
+
status: WatchdogStatus;
|
|
15
|
+
lastActivity?: Date;
|
|
16
|
+
blockedOn?: string;
|
|
17
|
+
details?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Patterns that indicate a permission prompt is blocking the agent
|
|
21
|
+
const PERMISSION_PROMPT_PATTERNS = [
|
|
22
|
+
"Permission required",
|
|
23
|
+
"Allow once",
|
|
24
|
+
"Allow always",
|
|
25
|
+
"Reject",
|
|
26
|
+
"Access external directory",
|
|
27
|
+
"△ Permission required",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Idle threshold: 2 minutes
|
|
31
|
+
const IDLE_THRESHOLD_MS = 2 * 60 * 1000;
|
|
32
|
+
// Stuck threshold: 5 minutes
|
|
33
|
+
const STUCK_THRESHOLD_MS = 5 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check agent progress by analyzing OpenCode logs and tmux output
|
|
37
|
+
*/
|
|
38
|
+
export function checkAgentProgress(agentName: string, containerName?: string): WatchdogResult {
|
|
39
|
+
// First check for permission prompts (highest priority)
|
|
40
|
+
const permissionPrompt = detectPermissionPrompt(agentName);
|
|
41
|
+
if (permissionPrompt) {
|
|
42
|
+
return {
|
|
43
|
+
status: "permission_blocked",
|
|
44
|
+
blockedOn: permissionPrompt,
|
|
45
|
+
details: "Agent is waiting for permission approval",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check last activity from OpenCode logs
|
|
50
|
+
const lastActivity = getLastActivityTime(agentName, containerName);
|
|
51
|
+
|
|
52
|
+
if (!lastActivity) {
|
|
53
|
+
// Can't determine activity, assume active
|
|
54
|
+
return {
|
|
55
|
+
status: "active",
|
|
56
|
+
details: "Unable to determine activity time",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timeSinceActivity = Date.now() - lastActivity.getTime();
|
|
61
|
+
|
|
62
|
+
if (timeSinceActivity > STUCK_THRESHOLD_MS) {
|
|
63
|
+
return {
|
|
64
|
+
status: "stuck",
|
|
65
|
+
lastActivity,
|
|
66
|
+
details: `No activity for ${Math.round(timeSinceActivity / 60000)} minutes`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (timeSinceActivity > IDLE_THRESHOLD_MS) {
|
|
71
|
+
return {
|
|
72
|
+
status: "idle",
|
|
73
|
+
lastActivity,
|
|
74
|
+
details: `Idle for ${Math.round(timeSinceActivity / 60000)} minutes`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: "active",
|
|
80
|
+
lastActivity,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect if a permission prompt is blocking the agent
|
|
86
|
+
*/
|
|
87
|
+
export function detectPermissionPrompt(agentName: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
const output = captureSessionOutput(agentName, 50);
|
|
90
|
+
if (!output) return null;
|
|
91
|
+
|
|
92
|
+
for (const pattern of PERMISSION_PROMPT_PATTERNS) {
|
|
93
|
+
if (output.includes(pattern)) {
|
|
94
|
+
// Try to extract what permission is being requested
|
|
95
|
+
const lines = output.split("\n");
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
if (line.includes("Access external directory")) {
|
|
98
|
+
const match = line.match(/Access external directory\s+(\S+)/);
|
|
99
|
+
if (match) return `External directory: ${match[1]}`;
|
|
100
|
+
}
|
|
101
|
+
if (line.includes("Permission required")) {
|
|
102
|
+
return "Permission prompt detected";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return "Permission prompt detected";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the last activity time from OpenCode logs
|
|
117
|
+
*/
|
|
118
|
+
export function getLastActivityTime(agentName: string, containerName?: string): Date | null {
|
|
119
|
+
try {
|
|
120
|
+
let logLine: string;
|
|
121
|
+
|
|
122
|
+
if (containerName) {
|
|
123
|
+
// Sandbox mode: read from container
|
|
124
|
+
const result = spawnSync(
|
|
125
|
+
"docker",
|
|
126
|
+
[
|
|
127
|
+
"exec",
|
|
128
|
+
containerName,
|
|
129
|
+
"sh",
|
|
130
|
+
"-c",
|
|
131
|
+
"ls -t /home/node/.local/share/opencode/log/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null",
|
|
132
|
+
],
|
|
133
|
+
{ encoding: "utf-8", timeout: 5000 },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
logLine = result.stdout.trim();
|
|
140
|
+
} else {
|
|
141
|
+
// Non-sandbox mode: read from local logs
|
|
142
|
+
const logDir = `${process.env.HOME}/.local/share/opencode/log`;
|
|
143
|
+
const result = spawnSync(
|
|
144
|
+
"sh",
|
|
145
|
+
["-c", `ls -t ${logDir}/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null`],
|
|
146
|
+
{ encoding: "utf-8", timeout: 5000 },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
logLine = result.stdout.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse timestamp from log line
|
|
156
|
+
// Format: INFO 2026-02-26T00:14:42 +0ms service=...
|
|
157
|
+
const match = logLine.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/);
|
|
158
|
+
if (match) {
|
|
159
|
+
return new Date(match[1] + "Z");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Send a nudge to the agent to continue working
|
|
170
|
+
*/
|
|
171
|
+
export function sendNudge(agentName: string, message: string): boolean {
|
|
172
|
+
try {
|
|
173
|
+
const sessionName = `agentmesh-${agentName}`;
|
|
174
|
+
|
|
175
|
+
// Send keys to tmux session
|
|
176
|
+
execSync(`tmux send-keys -t "${sessionName}" "${message.replace(/"/g, '\\"')}" Enter`, {
|
|
177
|
+
encoding: "utf-8",
|
|
178
|
+
timeout: 5000,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a process is actually running
|
|
189
|
+
*/
|
|
190
|
+
export function isProcessRunning(pid: number): boolean {
|
|
191
|
+
try {
|
|
192
|
+
process.kill(pid, 0);
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Find orphan containers for an agent
|
|
201
|
+
*/
|
|
202
|
+
export function findOrphanContainers(agentName: string): string[] {
|
|
203
|
+
try {
|
|
204
|
+
const result = spawnSync(
|
|
205
|
+
"docker",
|
|
206
|
+
["ps", "-aq", "--filter", `name=agentmesh-sandbox-${agentName}-`],
|
|
207
|
+
{ encoding: "utf-8", timeout: 5000 },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result.stdout.trim().split("\n").filter(Boolean);
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Remove orphan containers for an agent
|
|
222
|
+
*/
|
|
223
|
+
export function cleanupOrphanContainers(agentName: string): number {
|
|
224
|
+
const containers = findOrphanContainers(agentName);
|
|
225
|
+
|
|
226
|
+
for (const containerId of containers) {
|
|
227
|
+
try {
|
|
228
|
+
spawnSync("docker", ["rm", "-f", containerId], {
|
|
229
|
+
encoding: "utf-8",
|
|
230
|
+
timeout: 10000,
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore cleanup errors
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return containers.length;
|
|
238
|
+
}
|
package/src/core/websocket.ts
CHANGED
|
@@ -70,11 +70,11 @@ export class AgentWebSocket {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
this.reconnectAttempts++;
|
|
73
|
-
const delay = this.reconnectDelay *
|
|
73
|
+
const delay = this.reconnectDelay * 2 ** (this.reconnectAttempts - 1);
|
|
74
74
|
|
|
75
75
|
setTimeout(() => {
|
|
76
76
|
console.log(
|
|
77
|
-
`Reconnecting... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})
|
|
77
|
+
`Reconnecting... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
|
78
78
|
);
|
|
79
79
|
this.connect();
|
|
80
80
|
}, delay);
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Re-export core modules for programmatic usage
|
|
2
|
+
|
|
3
|
+
export * from "./config/loader.js";
|
|
4
|
+
export * from "./config/schema.js";
|
|
2
5
|
export { AgentDaemon } from "./core/daemon.js";
|
|
3
|
-
export { AgentWebSocket } from "./core/websocket.js";
|
|
4
6
|
export { Heartbeat } from "./core/heartbeat.js";
|
|
5
|
-
export * from "./core/tmux.js";
|
|
6
|
-
export * from "./core/registry.js";
|
|
7
7
|
export * from "./core/injector.js";
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
8
|
+
export * from "./core/registry.js";
|
|
9
|
+
export * from "./core/tmux.js";
|
|
10
|
+
export { AgentWebSocket } from "./core/websocket.js";
|