@agentmeshhq/agent 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +39 -0
  2. package/dist/__tests__/context-template.test.d.ts +4 -0
  3. package/dist/__tests__/context-template.test.js +233 -0
  4. package/dist/__tests__/context-template.test.js.map +1 -0
  5. package/dist/__tests__/loader.test.js +140 -28
  6. package/dist/__tests__/loader.test.js.map +1 -1
  7. package/dist/__tests__/no-respawn.test.d.ts +1 -0
  8. package/dist/__tests__/no-respawn.test.js +254 -0
  9. package/dist/__tests__/no-respawn.test.js.map +1 -0
  10. package/dist/__tests__/onboard.test.d.ts +5 -0
  11. package/dist/__tests__/onboard.test.js +341 -0
  12. package/dist/__tests__/onboard.test.js.map +1 -0
  13. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  14. package/dist/__tests__/orphan-process.test.js +286 -0
  15. package/dist/__tests__/orphan-process.test.js.map +1 -0
  16. package/dist/__tests__/runner.test.js +16 -0
  17. package/dist/__tests__/runner.test.js.map +1 -1
  18. package/dist/__tests__/shared-resource-guards.test.d.ts +7 -0
  19. package/dist/__tests__/shared-resource-guards.test.js +260 -0
  20. package/dist/__tests__/shared-resource-guards.test.js.map +1 -0
  21. package/dist/__tests__/watchdog.test.js +138 -12
  22. package/dist/__tests__/watchdog.test.js.map +1 -1
  23. package/dist/cli/status.js +11 -0
  24. package/dist/cli/status.js.map +1 -1
  25. package/dist/cli/stop.js +7 -2
  26. package/dist/cli/stop.js.map +1 -1
  27. package/dist/config/loader.d.ts +0 -4
  28. package/dist/config/loader.js +102 -42
  29. package/dist/config/loader.js.map +1 -1
  30. package/dist/config/schema.d.ts +6 -4
  31. package/dist/core/daemon/assignment-message.d.ts +12 -0
  32. package/dist/core/daemon/assignment-message.js +36 -0
  33. package/dist/core/daemon/assignment-message.js.map +1 -0
  34. package/dist/core/daemon/bootstrap.d.ts +35 -0
  35. package/dist/core/daemon/bootstrap.js +52 -0
  36. package/dist/core/daemon/bootstrap.js.map +1 -0
  37. package/dist/core/daemon/context-template.d.ts +11 -0
  38. package/dist/core/daemon/context-template.js +144 -0
  39. package/dist/core/daemon/context-template.js.map +1 -0
  40. package/dist/core/daemon/crash-log.d.ts +14 -0
  41. package/dist/core/daemon/crash-log.js +23 -0
  42. package/dist/core/daemon/crash-log.js.map +1 -0
  43. package/dist/core/daemon/git-auth.d.ts +18 -0
  44. package/dist/core/daemon/git-auth.js +88 -0
  45. package/dist/core/daemon/git-auth.js.map +1 -0
  46. package/dist/core/daemon/health-policy.d.ts +17 -0
  47. package/dist/core/daemon/health-policy.js +24 -0
  48. package/dist/core/daemon/health-policy.js.map +1 -0
  49. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  50. package/dist/core/daemon/sandbox-config.js +17 -0
  51. package/dist/core/daemon/sandbox-config.js.map +1 -0
  52. package/dist/core/daemon/state.d.ts +33 -0
  53. package/dist/core/daemon/state.js +78 -0
  54. package/dist/core/daemon/state.js.map +1 -0
  55. package/dist/core/daemon/tmux-session.d.ts +17 -0
  56. package/dist/core/daemon/tmux-session.js +34 -0
  57. package/dist/core/daemon/tmux-session.js.map +1 -0
  58. package/dist/core/daemon/workspace.d.ts +23 -0
  59. package/dist/core/daemon/workspace.js +90 -0
  60. package/dist/core/daemon/workspace.js.map +1 -0
  61. package/dist/core/daemon.d.ts +9 -12
  62. package/dist/core/daemon.js +293 -393
  63. package/dist/core/daemon.js.map +1 -1
  64. package/dist/core/injector.d.ts +5 -1
  65. package/dist/core/injector.js +83 -0
  66. package/dist/core/injector.js.map +1 -1
  67. package/dist/core/registry.d.ts +62 -0
  68. package/dist/core/registry.js +18 -0
  69. package/dist/core/registry.js.map +1 -1
  70. package/dist/core/runner/build.d.ts +9 -0
  71. package/dist/core/runner/build.js +53 -0
  72. package/dist/core/runner/build.js.map +1 -0
  73. package/dist/core/runner/detect.d.ts +5 -0
  74. package/dist/core/runner/detect.js +14 -0
  75. package/dist/core/runner/detect.js.map +1 -0
  76. package/dist/core/runner/index.d.ts +5 -0
  77. package/dist/core/runner/index.js +5 -0
  78. package/dist/core/runner/index.js.map +1 -0
  79. package/dist/core/runner/model.d.ts +5 -0
  80. package/dist/core/runner/model.js +7 -0
  81. package/dist/core/runner/model.js.map +1 -0
  82. package/dist/core/runner/opencode-models.d.ts +15 -0
  83. package/dist/core/runner/opencode-models.js +70 -0
  84. package/dist/core/runner/opencode-models.js.map +1 -0
  85. package/dist/core/runner/types.d.ts +19 -0
  86. package/dist/core/runner/types.js +8 -0
  87. package/dist/core/runner/types.js.map +1 -0
  88. package/dist/core/runner.d.ts +5 -47
  89. package/dist/core/runner.js +5 -167
  90. package/dist/core/runner.js.map +1 -1
  91. package/dist/core/tmux-runtime.d.ts +13 -0
  92. package/dist/core/tmux-runtime.js +72 -0
  93. package/dist/core/tmux-runtime.js.map +1 -0
  94. package/dist/core/tmux.d.ts +7 -1
  95. package/dist/core/tmux.js +75 -45
  96. package/dist/core/tmux.js.map +1 -1
  97. package/dist/core/watchdog.d.ts +18 -1
  98. package/dist/core/watchdog.js +78 -29
  99. package/dist/core/watchdog.js.map +1 -1
  100. package/package.json +24 -4
  101. package/src/__tests__/context.test.ts +0 -464
  102. package/src/__tests__/injector.test.ts +0 -29
  103. package/src/__tests__/jwt.test.ts +0 -112
  104. package/src/__tests__/loader.test.ts +0 -239
  105. package/src/__tests__/runner.test.ts +0 -104
  106. package/src/__tests__/sandbox.test.ts +0 -435
  107. package/src/__tests__/watchdog.test.ts +0 -368
  108. package/src/cli/attach.ts +0 -22
  109. package/src/cli/build.ts +0 -145
  110. package/src/cli/config.ts +0 -148
  111. package/src/cli/context.ts +0 -231
  112. package/src/cli/deploy.ts +0 -155
  113. package/src/cli/index.ts +0 -376
  114. package/src/cli/init.ts +0 -75
  115. package/src/cli/list.ts +0 -70
  116. package/src/cli/local.ts +0 -183
  117. package/src/cli/logs.ts +0 -64
  118. package/src/cli/migrate.ts +0 -212
  119. package/src/cli/nudge.ts +0 -81
  120. package/src/cli/restart.ts +0 -59
  121. package/src/cli/slack.ts +0 -70
  122. package/src/cli/start.ts +0 -118
  123. package/src/cli/status.ts +0 -91
  124. package/src/cli/stop.ts +0 -48
  125. package/src/cli/test.ts +0 -143
  126. package/src/cli/token.ts +0 -188
  127. package/src/cli/whoami.ts +0 -142
  128. package/src/config/loader.ts +0 -121
  129. package/src/config/schema.ts +0 -68
  130. package/src/context/handoff.ts +0 -122
  131. package/src/context/index.ts +0 -8
  132. package/src/context/schema.ts +0 -111
  133. package/src/context/storage.ts +0 -197
  134. package/src/core/daemon.ts +0 -1317
  135. package/src/core/heartbeat.ts +0 -129
  136. package/src/core/injector.ts +0 -292
  137. package/src/core/registry.ts +0 -159
  138. package/src/core/runner.ts +0 -225
  139. package/src/core/sandbox.ts +0 -547
  140. package/src/core/session-id.ts +0 -111
  141. package/src/core/tmux.ts +0 -405
  142. package/src/core/watchdog.ts +0 -238
  143. package/src/core/websocket.ts +0 -94
  144. package/src/index.ts +0 -10
  145. package/src/utils/jwt.ts +0 -87
  146. package/tsconfig.json +0 -8
  147. package/vitest.config.ts +0 -12
package/src/core/tmux.ts DELETED
@@ -1,405 +0,0 @@
1
- import { execFileSync, execSync, spawn } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- const SESSION_PREFIX = "agentmesh-";
7
-
8
- export function getSessionName(agentName: string): string {
9
- return `${SESSION_PREFIX}${agentName}`;
10
- }
11
-
12
- export function sessionExists(sessionName: string): boolean {
13
- try {
14
- execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`);
15
- return true;
16
- } catch {
17
- return false;
18
- }
19
- }
20
-
21
- export interface SessionEnv {
22
- AGENT_TOKEN?: string;
23
- AGENTMESH_AGENT_ID?: string;
24
- OPENCODE_MODEL?: string;
25
- CLAUDE_MODEL?: string;
26
- [key: string]: string | undefined;
27
- }
28
-
29
- export function createSession(
30
- agentName: string,
31
- command: string,
32
- workdir?: string,
33
- env?: SessionEnv,
34
- opencodeSessionId?: string,
35
- ): boolean {
36
- const sessionName = getSessionName(agentName);
37
-
38
- if (sessionExists(sessionName)) {
39
- console.error(`Session ${sessionName} already exists`);
40
- return false;
41
- }
42
-
43
- try {
44
- // Isolate OpenCode's SQLite database per agent to prevent WAL corruption
45
- // from multiple concurrent processes sharing one opencode.db file.
46
- // See docs/RCA-OPENCODE-SQLITE-CORRUPTION.md for details.
47
- const agentDataDir = path.join(os.homedir(), ".agentmesh", "opencode-data", agentName);
48
- const agentOpencodeDir = path.join(agentDataDir, "opencode");
49
- if (!fs.existsSync(agentOpencodeDir)) {
50
- fs.mkdirSync(agentOpencodeDir, { recursive: true });
51
- }
52
-
53
- // Copy auth.json from default OpenCode data dir so agents inherit API keys.
54
- // Strips xAI provider to prevent OpenCode from defaulting to non-Anthropic models.
55
- const agentAuthPath = path.join(agentOpencodeDir, "auth.json");
56
- const sourceAuthPath = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
57
- if (!fs.existsSync(agentAuthPath) && fs.existsSync(sourceAuthPath)) {
58
- try {
59
- const auth = JSON.parse(fs.readFileSync(sourceAuthPath, "utf-8"));
60
- delete auth.xai;
61
- fs.writeFileSync(agentAuthPath, JSON.stringify(auth, null, 2));
62
- } catch {
63
- // Non-fatal — agent will just need manual auth
64
- }
65
- }
66
-
67
- // Build environment prefix for the command
68
- // This ensures env vars are set BEFORE the process starts
69
- const mergedEnv: SessionEnv = {
70
- XDG_DATA_HOME: agentDataDir,
71
- ...env,
72
- };
73
-
74
- let envPrefix = "";
75
- const envParts: string[] = [];
76
- for (const [key, value] of Object.entries(mergedEnv)) {
77
- if (value !== undefined && value !== "") {
78
- // Escape special characters in value
79
- const escapedValue = value.replace(/"/g, '\\"');
80
- envParts.push(`${key}="${escapedValue}"`);
81
- }
82
- }
83
- if (envParts.length > 0) {
84
- envPrefix = `${envParts.join(" ")} `;
85
- }
86
-
87
- // Append --model flag for opencode if OPENCODE_MODEL is set
88
- let finalCommand = command;
89
- if (env?.OPENCODE_MODEL && (command === "opencode" || command.startsWith("opencode "))) {
90
- // Check if --model is already in the command
91
- if (!command.includes("--model") && !command.includes("-m ")) {
92
- finalCommand = `${command} --model ${env.OPENCODE_MODEL}`;
93
- }
94
- }
95
-
96
- // Append --session --continue flags for native session resume
97
- if (
98
- opencodeSessionId &&
99
- (finalCommand === "opencode" || finalCommand.startsWith("opencode ")) &&
100
- !finalCommand.includes("--session") &&
101
- !finalCommand.includes("--continue")
102
- ) {
103
- finalCommand = `${finalCommand} --session ${opencodeSessionId} --continue`;
104
- console.log(`[TMUX] Resuming OpenCode session: ${opencodeSessionId}`);
105
- }
106
-
107
- const fullCommand = `${envPrefix}${finalCommand}`;
108
-
109
- // Set reasonable terminal size for TUI applications
110
- const args = ["new-session", "-d", "-s", sessionName, "-x", "200", "-y", "50"];
111
-
112
- if (workdir) {
113
- args.push("-c", workdir);
114
- }
115
-
116
- args.push(fullCommand);
117
-
118
- execFileSync("tmux", args);
119
-
120
- // Also set session environment for any subsequent processes/refreshes
121
- if (env) {
122
- setSessionEnvironment(sessionName, env);
123
- }
124
-
125
- return true;
126
- } catch (error) {
127
- console.error(`Failed to create tmux session: ${error}`);
128
- return false;
129
- }
130
- }
131
-
132
- export function setSessionEnvironment(sessionName: string, env: SessionEnv): boolean {
133
- try {
134
- for (const [key, value] of Object.entries(env)) {
135
- if (value !== undefined && value !== "") {
136
- execFileSync("tmux", ["set-environment", "-t", sessionName, key, value]);
137
- }
138
- }
139
- return true;
140
- } catch (error) {
141
- console.error(`Failed to set session environment: ${error}`);
142
- return false;
143
- }
144
- }
145
-
146
- export function updateSessionEnvironment(agentName: string, env: SessionEnv): boolean {
147
- const sessionName = getSessionName(agentName);
148
-
149
- if (!sessionExists(sessionName)) {
150
- console.error(`Session ${sessionName} does not exist`);
151
- return false;
152
- }
153
-
154
- return setSessionEnvironment(sessionName, env);
155
- }
156
-
157
- export function destroySession(agentName: string): boolean {
158
- const sessionName = getSessionName(agentName);
159
-
160
- if (!sessionExists(sessionName)) {
161
- return true; // Already gone
162
- }
163
-
164
- try {
165
- execSync(`tmux kill-session -t "${sessionName}"`);
166
- return true;
167
- } catch (error) {
168
- console.error(`Failed to destroy tmux session: ${error}`);
169
- return false;
170
- }
171
- }
172
-
173
- export function sendKeys(agentName: string, message: string): boolean {
174
- const sessionName = getSessionName(agentName);
175
-
176
- if (!sessionExists(sessionName)) {
177
- console.error(`Session ${sessionName} does not exist`);
178
- return false;
179
- }
180
-
181
- try {
182
- // Replace newlines with " | " to keep message on single line (newlines would act as Enter)
183
- const cleanMessage = message.replace(/\n/g, " | ");
184
-
185
- // Use execFileSync with array args to avoid shell escaping issues
186
- // The -l flag sends keys literally
187
- execFileSync("tmux", ["send-keys", "-t", sessionName, "-l", cleanMessage]);
188
- execFileSync("tmux", ["send-keys", "-t", sessionName, "Enter"]);
189
- return true;
190
- } catch (error) {
191
- console.error(`Failed to send keys: ${error}`);
192
- return false;
193
- }
194
- }
195
-
196
- export function attachSession(agentName: string): void {
197
- const sessionName = getSessionName(agentName);
198
-
199
- if (!sessionExists(sessionName)) {
200
- console.error(`Session ${sessionName} does not exist`);
201
- process.exit(1);
202
- }
203
-
204
- // Attach to the session (replaces current process)
205
- const tmux = spawn("tmux", ["attach-session", "-t", sessionName], {
206
- stdio: "inherit",
207
- });
208
-
209
- tmux.on("exit", (code) => {
210
- process.exit(code ?? 0);
211
- });
212
- }
213
-
214
- export function listSessions(): string[] {
215
- try {
216
- const output = execSync("tmux list-sessions -F '#{session_name}'", {
217
- encoding: "utf-8",
218
- });
219
- return output
220
- .trim()
221
- .split("\n")
222
- .filter((s) => s.startsWith(SESSION_PREFIX))
223
- .map((s) => s.replace(SESSION_PREFIX, ""));
224
- } catch {
225
- return [];
226
- }
227
- }
228
-
229
- export function getSessionInfo(agentName: string): { exists: boolean; command?: string } {
230
- const sessionName = getSessionName(agentName);
231
-
232
- if (!sessionExists(sessionName)) {
233
- return { exists: false };
234
- }
235
-
236
- try {
237
- const command = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_current_command}"`, {
238
- encoding: "utf-8",
239
- }).trim();
240
-
241
- return { exists: true, command };
242
- } catch {
243
- return { exists: true };
244
- }
245
- }
246
-
247
- /**
248
- * Checks if the session process is healthy (not crashed/exited)
249
- * Returns true if healthy, false if crashed or dead
250
- * @param containerName - If provided, checks inside the Docker container
251
- */
252
- export function isSessionHealthy(
253
- agentName: string,
254
- containerName?: string,
255
- ): { healthy: boolean; reason?: string } {
256
- const sessionName = getSessionName(agentName);
257
-
258
- // For sandbox mode, check tmux inside the container
259
- if (containerName) {
260
- try {
261
- // Check if container is running
262
- const containerStatus = execSync(
263
- `docker inspect -f '{{.State.Running}}' "${containerName}"`,
264
- {
265
- encoding: "utf-8",
266
- },
267
- ).trim();
268
-
269
- if (containerStatus !== "true") {
270
- return { healthy: false, reason: "container_not_running" };
271
- }
272
-
273
- // Check if tmux session exists inside container
274
- const tmuxCheck = execSync(
275
- `docker exec "${containerName}" tmux has-session -t "${sessionName}" 2>&1 || echo "no_session"`,
276
- { encoding: "utf-8" },
277
- ).trim();
278
-
279
- if (tmuxCheck === "no_session" || tmuxCheck.includes("no server running")) {
280
- return { healthy: false, reason: "session_not_found_in_container" };
281
- }
282
-
283
- // Check if pane is alive inside container
284
- const paneInfo = execSync(
285
- `docker exec "${containerName}" tmux list-panes -t "${sessionName}" -F "#{pane_pid}:#{pane_dead}"`,
286
- { encoding: "utf-8" },
287
- ).trim();
288
-
289
- const [pid, dead] = paneInfo.split(":");
290
-
291
- if (dead === "1") {
292
- return { healthy: false, reason: "pane_dead_in_container" };
293
- }
294
-
295
- if (!pid || pid === "0") {
296
- return { healthy: false, reason: "no_pid_in_container" };
297
- }
298
-
299
- return { healthy: true };
300
- } catch (error) {
301
- return { healthy: false, reason: `container_check_failed: ${error}` };
302
- }
303
- }
304
-
305
- // Local mode - check tmux on host
306
- if (!sessionExists(sessionName)) {
307
- return { healthy: false, reason: "session_not_found" };
308
- }
309
-
310
- try {
311
- // Check if pane is still alive and has a running process
312
- const paneInfo = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_pid}:#{pane_dead}"`, {
313
- encoding: "utf-8",
314
- }).trim();
315
-
316
- const [pid, dead] = paneInfo.split(":");
317
-
318
- if (dead === "1") {
319
- return { healthy: false, reason: "pane_dead" };
320
- }
321
-
322
- if (!pid || pid === "0") {
323
- return { healthy: false, reason: "no_pid" };
324
- }
325
-
326
- return { healthy: true };
327
- } catch (error) {
328
- return { healthy: false, reason: `check_failed: ${error}` };
329
- }
330
- }
331
-
332
- export interface SessionContext {
333
- workdir: string;
334
- gitBranch?: string;
335
- gitStatus?: string;
336
- }
337
-
338
- /**
339
- * Captures current working state from the tmux session
340
- */
341
- export function captureSessionContext(agentName: string): SessionContext | null {
342
- const sessionName = getSessionName(agentName);
343
-
344
- if (!sessionExists(sessionName)) {
345
- return null;
346
- }
347
-
348
- try {
349
- // Get current working directory from the pane
350
- const workdir = execSync(`tmux display-message -t "${sessionName}" -p "#{pane_current_path}"`, {
351
- encoding: "utf-8",
352
- }).trim();
353
-
354
- const context: SessionContext = { workdir };
355
-
356
- // Try to get git info if in a git repo
357
- try {
358
- const gitBranch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
359
- encoding: "utf-8",
360
- cwd: workdir,
361
- }).trim();
362
- if (gitBranch) {
363
- context.gitBranch = gitBranch;
364
- }
365
-
366
- const gitStatus = execSync("git status --short 2>/dev/null", {
367
- encoding: "utf-8",
368
- cwd: workdir,
369
- }).trim();
370
- if (gitStatus) {
371
- // Truncate if too long
372
- context.gitStatus =
373
- gitStatus.length > 500 ? `${gitStatus.substring(0, 500)}...` : gitStatus;
374
- }
375
- } catch {
376
- // Not a git repo or git not available
377
- }
378
-
379
- return context;
380
- } catch (error) {
381
- console.error(`Failed to capture session context: ${error}`);
382
- return null;
383
- }
384
- }
385
-
386
- /**
387
- * Captures recent output from the tmux pane (last N lines)
388
- */
389
- export function captureSessionOutput(agentName: string, lines = 100): string | null {
390
- const sessionName = getSessionName(agentName);
391
-
392
- if (!sessionExists(sessionName)) {
393
- return null;
394
- }
395
-
396
- try {
397
- const output = execSync(`tmux capture-pane -t "${sessionName}" -p -S -${lines}`, {
398
- encoding: "utf-8",
399
- });
400
- return output;
401
- } catch (error) {
402
- console.error(`Failed to capture session output: ${error}`);
403
- return null;
404
- }
405
- }
@@ -1,238 +0,0 @@
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
- }