@agentmeshhq/agent 0.2.0 → 0.2.1

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  4. package/dist/__tests__/orphan-process.test.js +286 -0
  5. package/dist/__tests__/orphan-process.test.js.map +1 -0
  6. package/dist/__tests__/runner.test.js +16 -0
  7. package/dist/__tests__/runner.test.js.map +1 -1
  8. package/dist/__tests__/watchdog.test.js +138 -12
  9. package/dist/__tests__/watchdog.test.js.map +1 -1
  10. package/dist/cli/index.js +0 -0
  11. package/dist/cli/status.js +11 -0
  12. package/dist/cli/status.js.map +1 -1
  13. package/dist/cli/stop.js +7 -2
  14. package/dist/cli/stop.js.map +1 -1
  15. package/dist/config/schema.d.ts +4 -2
  16. package/dist/core/daemon/assignment-message.d.ts +12 -0
  17. package/dist/core/daemon/assignment-message.js +36 -0
  18. package/dist/core/daemon/assignment-message.js.map +1 -0
  19. package/dist/core/daemon/bootstrap.d.ts +35 -0
  20. package/dist/core/daemon/bootstrap.js +52 -0
  21. package/dist/core/daemon/bootstrap.js.map +1 -0
  22. package/dist/core/daemon/crash-log.d.ts +16 -0
  23. package/dist/core/daemon/crash-log.js +24 -0
  24. package/dist/core/daemon/crash-log.js.map +1 -0
  25. package/dist/core/daemon/health-policy.d.ts +21 -0
  26. package/dist/core/daemon/health-policy.js +32 -0
  27. package/dist/core/daemon/health-policy.js.map +1 -0
  28. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  29. package/dist/core/daemon/sandbox-config.js +17 -0
  30. package/dist/core/daemon/sandbox-config.js.map +1 -0
  31. package/dist/core/daemon/state.d.ts +33 -0
  32. package/dist/core/daemon/state.js +77 -0
  33. package/dist/core/daemon/state.js.map +1 -0
  34. package/dist/core/daemon/tmux-session.d.ts +17 -0
  35. package/dist/core/daemon/tmux-session.js +34 -0
  36. package/dist/core/daemon/tmux-session.js.map +1 -0
  37. package/dist/core/daemon/workspace.d.ts +10 -0
  38. package/dist/core/daemon/workspace.js +51 -0
  39. package/dist/core/daemon/workspace.js.map +1 -0
  40. package/dist/core/daemon.d.ts +0 -6
  41. package/dist/core/daemon.js +123 -244
  42. package/dist/core/daemon.js.map +1 -1
  43. package/dist/core/injector.js +6 -0
  44. package/dist/core/injector.js.map +1 -1
  45. package/dist/core/runner/build.d.ts +9 -0
  46. package/dist/core/runner/build.js +53 -0
  47. package/dist/core/runner/build.js.map +1 -0
  48. package/dist/core/runner/detect.d.ts +5 -0
  49. package/dist/core/runner/detect.js +14 -0
  50. package/dist/core/runner/detect.js.map +1 -0
  51. package/dist/core/runner/index.d.ts +5 -0
  52. package/dist/core/runner/index.js +5 -0
  53. package/dist/core/runner/index.js.map +1 -0
  54. package/dist/core/runner/model.d.ts +5 -0
  55. package/dist/core/runner/model.js +7 -0
  56. package/dist/core/runner/model.js.map +1 -0
  57. package/dist/core/runner/opencode-models.d.ts +15 -0
  58. package/dist/core/runner/opencode-models.js +70 -0
  59. package/dist/core/runner/opencode-models.js.map +1 -0
  60. package/dist/core/runner/types.d.ts +19 -0
  61. package/dist/core/runner/types.js +8 -0
  62. package/dist/core/runner/types.js.map +1 -0
  63. package/dist/core/runner.d.ts +5 -47
  64. package/dist/core/runner.js +5 -167
  65. package/dist/core/runner.js.map +1 -1
  66. package/dist/core/tmux-runtime.d.ts +13 -0
  67. package/dist/core/tmux-runtime.js +72 -0
  68. package/dist/core/tmux-runtime.js.map +1 -0
  69. package/dist/core/tmux.d.ts +7 -1
  70. package/dist/core/tmux.js +75 -45
  71. package/dist/core/tmux.js.map +1 -1
  72. package/dist/core/watchdog.d.ts +18 -1
  73. package/dist/core/watchdog.js +78 -29
  74. package/dist/core/watchdog.js.map +1 -1
  75. package/package.json +30 -11
  76. package/dist/cli/inbox.d.ts +0 -5
  77. package/dist/cli/inbox.js +0 -123
  78. package/dist/cli/inbox.js.map +0 -1
  79. package/dist/cli/issue.d.ts +0 -42
  80. package/dist/cli/issue.js +0 -297
  81. package/dist/cli/issue.js.map +0 -1
  82. package/dist/cli/ready.d.ts +0 -5
  83. package/dist/cli/ready.js +0 -131
  84. package/dist/cli/ready.js.map +0 -1
  85. package/dist/cli/sync.d.ts +0 -8
  86. package/dist/cli/sync.js +0 -154
  87. package/dist/cli/sync.js.map +0 -1
  88. package/dist/core/issue-cache.d.ts +0 -44
  89. package/dist/core/issue-cache.js +0 -75
  90. package/dist/core/issue-cache.js.map +0 -1
  91. package/src/__tests__/context.test.ts +0 -464
  92. package/src/__tests__/injector.test.ts +0 -29
  93. package/src/__tests__/jwt.test.ts +0 -112
  94. package/src/__tests__/loader.test.ts +0 -239
  95. package/src/__tests__/runner.test.ts +0 -104
  96. package/src/__tests__/sandbox.test.ts +0 -435
  97. package/src/__tests__/watchdog.test.ts +0 -368
  98. package/src/cli/attach.ts +0 -22
  99. package/src/cli/build.ts +0 -145
  100. package/src/cli/config.ts +0 -148
  101. package/src/cli/context.ts +0 -231
  102. package/src/cli/deploy.ts +0 -155
  103. package/src/cli/index.ts +0 -376
  104. package/src/cli/init.ts +0 -75
  105. package/src/cli/list.ts +0 -70
  106. package/src/cli/local.ts +0 -183
  107. package/src/cli/logs.ts +0 -64
  108. package/src/cli/migrate.ts +0 -212
  109. package/src/cli/nudge.ts +0 -81
  110. package/src/cli/restart.ts +0 -59
  111. package/src/cli/slack.ts +0 -70
  112. package/src/cli/start.ts +0 -118
  113. package/src/cli/status.ts +0 -91
  114. package/src/cli/stop.ts +0 -48
  115. package/src/cli/test.ts +0 -143
  116. package/src/cli/token.ts +0 -188
  117. package/src/cli/whoami.ts +0 -142
  118. package/src/config/loader.ts +0 -121
  119. package/src/config/schema.ts +0 -68
  120. package/src/context/handoff.ts +0 -122
  121. package/src/context/index.ts +0 -8
  122. package/src/context/schema.ts +0 -111
  123. package/src/context/storage.ts +0 -197
  124. package/src/core/daemon.ts +0 -1317
  125. package/src/core/heartbeat.ts +0 -129
  126. package/src/core/injector.ts +0 -292
  127. package/src/core/registry.ts +0 -159
  128. package/src/core/runner.ts +0 -225
  129. package/src/core/sandbox.ts +0 -547
  130. package/src/core/session-id.ts +0 -111
  131. package/src/core/tmux.ts +0 -405
  132. package/src/core/watchdog.ts +0 -238
  133. package/src/core/websocket.ts +0 -94
  134. package/src/index.ts +0 -10
  135. package/src/utils/jwt.ts +0 -87
  136. package/tsconfig.json +0 -8
  137. package/vitest.config.ts +0 -12
@@ -0,0 +1,10 @@
1
+ export interface WorkspaceSetupInput {
2
+ workspacePath: string;
3
+ repoUrl: string;
4
+ defaultBranch: string;
5
+ projectName: string;
6
+ }
7
+ /**
8
+ * Ensures a project workspace exists and is on the target default branch.
9
+ */
10
+ export declare function setupWorkspace(input: WorkspaceSetupInput): string;
@@ -0,0 +1,51 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Ensures a project workspace exists and is on the target default branch.
6
+ */
7
+ export function setupWorkspace(input) {
8
+ const { workspacePath, repoUrl, defaultBranch, projectName } = input;
9
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
10
+ console.log(`🔧 AUTO-SETUP: Setting up workspace for ${projectName}`);
11
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
12
+ const gitDir = path.join(workspacePath, ".git");
13
+ if (fs.existsSync(gitDir)) {
14
+ console.log(`✓ Workspace already exists: ${workspacePath}`);
15
+ console.log(" Updating from remote...");
16
+ try {
17
+ execSync("git fetch origin", { cwd: workspacePath, stdio: "inherit" });
18
+ execSync(`git checkout ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
19
+ execSync(`git pull origin ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
20
+ console.log("✓ Workspace updated successfully\n");
21
+ }
22
+ catch (error) {
23
+ console.warn(`⚠ Could not update workspace: ${error.message}`);
24
+ console.log(" Continuing with existing state...\n");
25
+ }
26
+ return workspacePath;
27
+ }
28
+ const parentDir = path.dirname(workspacePath);
29
+ if (!fs.existsSync(parentDir)) {
30
+ console.log(`Creating directory: ${parentDir}`);
31
+ fs.mkdirSync(parentDir, { recursive: true });
32
+ }
33
+ console.log("Cloning repository...");
34
+ console.log(` URL: ${repoUrl}`);
35
+ console.log(` Path: ${workspacePath}`);
36
+ console.log(` Branch: ${defaultBranch}\n`);
37
+ try {
38
+ execSync(`git clone --branch ${defaultBranch} "${repoUrl}" "${workspacePath}"`, {
39
+ stdio: "inherit",
40
+ });
41
+ console.log("\n✓ Repository cloned successfully");
42
+ }
43
+ catch (error) {
44
+ console.error(`\n✗ Failed to clone repository: ${error.message}`);
45
+ console.error("\nMake sure you have access to the repository and SSH keys are configured.");
46
+ process.exit(1);
47
+ }
48
+ console.log(`✓ Workspace ready: ${workspacePath}\n`);
49
+ return workspacePath;
50
+ }
51
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/daemon/workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAS7B;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAErE,OAAO,CAAC,GAAG,CAAC,oFAAoF,CAAC,CAAC;IAClG,OAAO,CAAC,GAAG,CAAC,2CAA2C,WAAW,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,oFAAoF,CAAC,CAAC;IAElG,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,+BAA+B,aAAa,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAEzC,IAAI,CAAC;YACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,gBAAgB,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACpF,QAAQ,CAAC,mBAAmB,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACvF,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,iCAAkC,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YAC1E,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;QAChD,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,WAAW,aAAa,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,aAAa,aAAa,IAAI,CAAC,CAAC;IAE5C,IAAI,CAAC;QACH,QAAQ,CAAC,sBAAsB,aAAa,KAAK,OAAO,MAAM,aAAa,GAAG,EAAE;YAC9E,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mCAAoC,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,OAAO,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,aAAa,IAAI,CAAC,CAAC;IACrD,OAAO,aAAa,CAAC;AACvB,CAAC"}
@@ -46,7 +46,6 @@ export declare class AgentDaemon {
46
46
  private sandboxMemory;
47
47
  private sandbox;
48
48
  private healthCheckInterval;
49
- private serverContext;
50
49
  private _preStartSessionId;
51
50
  private _attemptedResumeSessionId;
52
51
  private restartCount;
@@ -96,11 +95,6 @@ export declare class AgentDaemon {
96
95
  * Uses project.workdir from HQ as source of truth, falls back to helpful instructions
97
96
  */
98
97
  private checkAssignments;
99
- /**
100
- * Sets up workspace by cloning repository or using existing clone
101
- * Returns the absolute path to the workspace
102
- */
103
- private setupWorkspace;
104
98
  /**
105
99
  * Ensures the sandbox OpenCode config exists
106
100
  * Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
@@ -1,16 +1,25 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { addAgentToState, getAgentState, loadConfig, resetAgentRestartCount, updateAgentInState, } from "../config/loader.js";
5
+ import { getAgentState, resetAgentRestartCount, updateAgentInState } from "../config/loader.js";
6
6
  import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
7
+ import { renderMissingWorkdirMessage } from "./daemon/assignment-message.js";
8
+ import { bootstrapDaemon } from "./daemon/bootstrap.js";
9
+ import { formatCrashLog } from "./daemon/crash-log.js";
10
+ import { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, shouldResetRestartCount, } from "./daemon/health-policy.js";
11
+ import { writeSandboxOpencodeConfig } from "./daemon/sandbox-config.js";
12
+ import { captureAgentChildPids, persistRunningState } from "./daemon/state.js";
13
+ import { startTmuxRuntimeSession } from "./daemon/tmux-session.js";
14
+ import { setupWorkspace } from "./daemon/workspace.js";
7
15
  import { Heartbeat } from "./heartbeat.js";
8
16
  import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
9
17
  import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
10
- import { buildRunnerConfig, getRunnerDisplayName } from "./runner.js";
18
+ import { getRunnerDisplayName } from "./runner.js";
11
19
  import { DockerSandbox } from "./sandbox.js";
12
20
  import { getLatestSessionId, snapshotSessionId, waitForNewSessionId } from "./session-id.js";
13
- import { captureSessionContext, captureSessionOutput, createSession, destroySession, getSessionName, isSessionHealthy, sessionExists, updateSessionEnvironment, } from "./tmux.js";
21
+ import { captureSessionContext, captureSessionOutput, createSession, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
22
+ import { prepareOpenCodeRuntime } from "./tmux-runtime.js";
14
23
  import { checkAgentProgress, cleanupOrphanContainers, isProcessRunning, sendNudge, } from "./watchdog.js";
15
24
  import { AgentWebSocket } from "./websocket.js";
16
25
  // Maximum number of auto-restart attempts
@@ -49,7 +58,6 @@ export class AgentDaemon {
49
58
  sandboxMemory;
50
59
  sandbox = null;
51
60
  healthCheckInterval = null;
52
- serverContext;
53
61
  // Session resume tracking
54
62
  _preStartSessionId;
55
63
  _attemptedResumeSessionId;
@@ -59,51 +67,20 @@ export class AgentDaemon {
59
67
  stuckSince = null;
60
68
  nudgeSentAt = null;
61
69
  constructor(options) {
62
- const config = loadConfig();
63
- if (!config) {
64
- throw new Error("No config found. Run 'agentmesh init' first.");
65
- }
66
- // Ensure config has required fields with defaults
67
- if (!config.agents)
68
- config.agents = [];
69
- if (!config.defaults)
70
- config.defaults = { command: "opencode", model: "claude-sonnet-4-5-20250929" };
71
- this.config = config;
70
+ const boot = bootstrapDaemon(options);
71
+ this.config = boot.config;
72
72
  this.agentName = options.name;
73
- this.shouldRestoreContext = options.restoreContext === true;
74
- this.isWorkerAgent = options.worker === true;
75
- this.autoSetup = options.autoSetup === true;
76
- // Find or create agent config
77
- let agentConfig = config.agents.find((a) => a.name === options.name);
78
- if (!agentConfig) {
79
- agentConfig = {
80
- name: options.name,
81
- command: options.command || config.defaults.command,
82
- workdir: options.workdir,
83
- model: options.model || config.defaults.model,
84
- };
85
- }
86
- // Override with provided options
87
- if (options.command)
88
- agentConfig.command = options.command;
89
- if (options.workdir)
90
- agentConfig.workdir = options.workdir;
91
- if (options.model)
92
- agentConfig.model = options.model;
93
- this.agentConfig = agentConfig;
94
- this.serveMode = options.serve === true;
95
- this.servePort = options.servePort || 3001;
96
- this.sandboxMode = options.sandbox === true;
97
- this.sandboxImage = options.sandboxImage || "agentmesh/agent-sandbox:latest";
98
- this.sandboxCpu = options.sandboxCpu || "1";
99
- this.sandboxMemory = options.sandboxMemory || "2g";
100
- // Build runner configuration with model resolution
101
- this.runnerConfig = buildRunnerConfig({
102
- cliModel: options.model,
103
- agentModel: agentConfig.model,
104
- defaultModel: config.defaults.model,
105
- command: agentConfig.command,
106
- });
73
+ this.shouldRestoreContext = boot.shouldRestoreContext;
74
+ this.isWorkerAgent = boot.isWorkerAgent;
75
+ this.autoSetup = boot.autoSetup;
76
+ this.agentConfig = boot.agentConfig;
77
+ this.serveMode = boot.serveMode;
78
+ this.servePort = boot.servePort;
79
+ this.sandboxMode = boot.sandboxMode;
80
+ this.sandboxImage = boot.sandboxImage;
81
+ this.sandboxCpu = boot.sandboxCpu;
82
+ this.sandboxMemory = boot.sandboxMemory;
83
+ this.runnerConfig = boot.runnerConfig;
107
84
  const runnerName = getRunnerDisplayName(this.runnerConfig.type);
108
85
  console.log(`Runner: ${runnerName}`);
109
86
  console.log(`Effective model: ${this.runnerConfig.model}`);
@@ -121,8 +98,13 @@ export class AgentDaemon {
121
98
  throw new Error(`Agent "${this.agentName}" is already running (PID: ${existingState.pid}). ` +
122
99
  `Use 'agentmesh stop ${this.agentName}' first.`);
123
100
  }
124
- // Process not running, clean up stale state
101
+ // Process not running clean up stale state and any orphaned child processes
125
102
  console.log(`Cleaning up stale state for PID ${existingState.pid}`);
103
+ const orphanPids = existingState.childPids ?? [];
104
+ if (orphanPids.length > 0) {
105
+ console.log(`[STARTUP] Found ${orphanPids.length} orphaned child PIDs from previous run — killing...`);
106
+ killProcessTree(orphanPids);
107
+ }
126
108
  }
127
109
  // Clean up orphan containers in sandbox mode
128
110
  if (this.sandboxMode) {
@@ -151,7 +133,6 @@ export class AgentDaemon {
151
133
  if (registration.status === "re-registered") {
152
134
  console.log(`Re-registered as: ${this.agentId}`);
153
135
  if (registration.context && Object.keys(registration.context).length > 0) {
154
- this.serverContext = registration.context;
155
136
  console.log(`Server context restored: ${Object.keys(registration.context).join(", ")}`);
156
137
  }
157
138
  }
@@ -168,39 +149,16 @@ export class AgentDaemon {
168
149
  await this.startServeMode();
169
150
  }
170
151
  else {
171
- // Check if session already exists
172
- const sessionName = getSessionName(this.agentName);
173
- const sessionAlreadyExists = sessionExists(sessionName);
174
- // Create tmux session if it doesn't exist
175
- if (!sessionAlreadyExists) {
176
- // Load saved context to check for OpenCode session ID (for native resume)
177
- let savedSessionId;
178
- if (this.shouldRestoreContext && this.agentId) {
179
- const savedContext = loadContext(this.agentId);
180
- savedSessionId = savedContext?.custom?.opencodeSessionId;
181
- if (savedSessionId) {
182
- console.log(`[CONTEXT] Found saved OpenCode session ID: ${savedSessionId}`);
183
- }
184
- }
185
- // Snapshot the latest session ID in logs BEFORE starting OpenCode.
186
- // This lets us detect whether OpenCode actually resumed vs created a new session.
187
- const preStartSessionId = snapshotSessionId(this.agentName);
188
- console.log(`Creating tmux session: ${sessionName}`);
189
- // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
190
- const created = createSession(this.agentName, this.agentConfig.command, this.agentConfig.workdir, this.runnerConfig.env, // Apply model env at process start
191
- savedSessionId);
192
- if (!created) {
193
- throw new Error("Failed to create tmux session");
194
- }
195
- // Store pre-start snapshot for fallback detection later
196
- this._preStartSessionId = preStartSessionId;
197
- this._attemptedResumeSessionId = savedSessionId;
198
- }
199
- else {
200
- console.log(`Reconnecting to existing session: ${sessionName}`);
201
- // Update environment for existing session
202
- updateSessionEnvironment(this.agentName, this.runnerConfig.env);
203
- }
152
+ const sessionStart = startTmuxRuntimeSession({
153
+ agentName: this.agentName,
154
+ agentId: this.agentId,
155
+ command: this.agentConfig.command,
156
+ workdir: this.agentConfig.workdir,
157
+ runnerEnv: this.runnerConfig.env,
158
+ shouldRestoreContext: this.shouldRestoreContext,
159
+ });
160
+ this._preStartSessionId = sessionStart.preStartSessionId;
161
+ this._attemptedResumeSessionId = sessionStart.attemptedResumeSessionId;
204
162
  // Inject environment variables into tmux session
205
163
  console.log("Injecting environment variables...");
206
164
  updateSessionEnvironment(this.agentName, {
@@ -209,20 +167,27 @@ export class AgentDaemon {
209
167
  });
210
168
  }
211
169
  // Save state including runtime model info
212
- const sessionName = this.serveMode ? `serve:${this.servePort}` : getSessionName(this.agentName);
213
- addAgentToState({
214
- name: this.agentName,
170
+ persistRunningState({
171
+ agentName: this.agentName,
215
172
  agentId: this.agentId,
216
173
  pid: process.pid,
217
- tmuxSession: sessionName,
218
- startedAt: new Date().toISOString(),
219
174
  token: this.token,
220
175
  workdir: this.agentConfig.workdir,
221
176
  assignedProject: this.assignedProject,
222
177
  runtimeModel: this.runnerConfig.model,
223
178
  runnerType: this.runnerConfig.type,
224
179
  sandboxContainer: this.sandbox?.getContainerName(),
180
+ serveMode: this.serveMode,
181
+ servePort: this.servePort,
225
182
  });
183
+ // Track child PIDs for cleanup on restart/stop (tmux mode only — sandbox/serve manage their own)
184
+ if (!this.sandboxMode && !this.serveMode) {
185
+ const childPids = captureAgentChildPids(this.agentName);
186
+ if (childPids.length > 0) {
187
+ updateAgentInState(this.agentName, { childPids });
188
+ console.log(`[STARTUP] Tracking ${childPids.length} child PIDs: ${childPids.join(", ")}`);
189
+ }
190
+ }
226
191
  // Start heartbeat with auto-refresh
227
192
  console.log("Starting heartbeat...");
228
193
  this.heartbeat = new Heartbeat({
@@ -404,13 +369,10 @@ Nudge agent:
404
369
  if (!this.isRunning)
405
370
  return;
406
371
  // Reset restart count after stable operation
407
- if (this.lastStableTime && this.restartCount > 0) {
408
- const stableTime = Date.now() - this.lastStableTime.getTime();
409
- if (stableTime > RESTART_COUNT_RESET_MS) {
410
- console.log(`[HEALTH] Agent stable for 30+ minutes, resetting restart count`);
411
- this.restartCount = 0;
412
- resetAgentRestartCount(this.agentName);
413
- }
372
+ if (shouldResetRestartCount(this.restartCount, this.lastStableTime, RESTART_COUNT_RESET_MS)) {
373
+ console.log(`[HEALTH] Agent stable for 30+ minutes, resetting restart count`);
374
+ this.restartCount = 0;
375
+ resetAgentRestartCount(this.agentName);
414
376
  }
415
377
  // For sandbox mode, pass container name so health check looks inside container
416
378
  const containerName = this.sandboxMode ? this.sandbox?.getContainerName() : undefined;
@@ -422,7 +384,17 @@ Nudge agent:
422
384
  }
423
385
  // Session is alive - check progress watchdog
424
386
  const progress = checkAgentProgress(this.agentName, containerName);
425
- if (progress.status === "permission_blocked" || progress.status === "stuck") {
387
+ if (progress.status === "waiting_for_human") {
388
+ // Agent is intentionally waiting for human input - do not classify as stuck
389
+ if (this.stuckSince) {
390
+ // Clear any prior stuck tracking since the agent signalled a legitimate wait
391
+ this.stuckSince = null;
392
+ this.nudgeSentAt = null;
393
+ updateAgentInState(this.agentName, { stuckSince: undefined, status: "waiting" });
394
+ }
395
+ console.log(`[HEALTH] Agent is waiting for human input: ${progress.details}`);
396
+ }
397
+ else if (progress.status === "permission_blocked" || progress.status === "stuck") {
426
398
  await this.handleStuckAgent(progress);
427
399
  }
428
400
  else if (progress.status === "active") {
@@ -451,24 +423,18 @@ Nudge agent:
451
423
  catch {
452
424
  lastOutput = "Failed to capture session output";
453
425
  }
454
- const crashLog = `
455
- ================================================================================
456
- AGENT CRASH DETECTED
457
- ================================================================================
458
- Timestamp: ${timestamp}
459
- Agent: ${this.agentName}
460
- Agent ID: ${this.agentId}
461
- Reason: ${reason}
462
- Restart Count: ${this.restartCount}/${MAX_RESTART_ATTEMPTS}
463
- Sandbox: ${this.sandboxMode ? this.sandbox?.getContainerName() : "none"}
464
- Workdir: ${this.agentConfig.workdir}
465
- Model: ${this.runnerConfig.model}
466
-
467
- --- Last Session Output ---
468
- ${lastOutput}
469
- ================================================================================
470
-
471
- `;
426
+ const crashLog = formatCrashLog({
427
+ timestamp,
428
+ agentName: this.agentName,
429
+ agentId: this.agentId,
430
+ reason,
431
+ restartCount: this.restartCount,
432
+ maxRestartAttempts: MAX_RESTART_ATTEMPTS,
433
+ sandboxLabel: this.sandboxMode ? this.sandbox?.getContainerName() || "sandbox" : "none",
434
+ workdir: this.agentConfig.workdir,
435
+ model: this.runnerConfig.model,
436
+ lastOutput,
437
+ });
472
438
  fs.appendFileSync(logFile, crashLog);
473
439
  // Save context (including session ID) before restart attempt
474
440
  if (this.agentId) {
@@ -517,7 +483,7 @@ ${lastOutput}
517
483
  if (!this.stuckSince) {
518
484
  // First detection of stuck state
519
485
  this.stuckSince = now;
520
- console.log(`[HEALTH] Agent appears stuck: ${progress.details || progress.blockedOn || "no activity"}`);
486
+ console.log(`[HEALTH] Agent appears stuck: ${getStuckDetail(progress)}`);
521
487
  updateAgentInState(this.agentName, {
522
488
  stuckSince: now.toISOString(),
523
489
  status: "stuck",
@@ -528,9 +494,7 @@ ${lastOutput}
528
494
  // If we haven't sent a nudge yet, send one
529
495
  if (!this.nudgeSentAt) {
530
496
  console.log(`[HEALTH] Sending nudge to worker agent...`);
531
- const nudgeMessage = progress.status === "permission_blocked"
532
- ? "Please continue with your task. If you see a permission prompt, try an alternative approach that doesn't require that permission."
533
- : "Please continue with your current task.";
497
+ const nudgeMessage = getNudgeMessage(progress);
534
498
  const sent = sendNudge(this.agentName, nudgeMessage);
535
499
  if (sent) {
536
500
  this.nudgeSentAt = now;
@@ -542,8 +506,7 @@ ${lastOutput}
542
506
  return;
543
507
  }
544
508
  // Check if enough time has passed since nudge
545
- const timeSinceNudge = now.getTime() - this.nudgeSentAt.getTime();
546
- if (timeSinceNudge < NUDGE_WAIT_MS) {
509
+ if (isWithinNudgeWaitWindow(this.nudgeSentAt, NUDGE_WAIT_MS, now)) {
547
510
  // Still waiting for agent to respond to nudge
548
511
  return;
549
512
  }
@@ -558,15 +521,19 @@ ${lastOutput}
558
521
  * Restarts the agent session (sandbox or non-sandbox)
559
522
  */
560
523
  async restartSession() {
561
- // Destroy existing session
562
- destroySession(this.agentName);
524
+ // Retrieve tracked child PIDs before destroying the session
525
+ const currentState = getAgentState(this.agentName);
526
+ const childPids = currentState?.childPids ?? [];
527
+ // Destroy existing session AND kill all tracked child processes
528
+ destroySession(this.agentName, childPids);
529
+ // Allow cleanup to settle before spawning a new session
530
+ await new Promise((resolve) => setTimeout(resolve, 1000));
563
531
  if (this.sandboxMode && this.sandbox) {
564
532
  // Restart sandbox container
565
533
  const newContainerId = await this.sandbox.restart();
566
534
  console.log(`[RESTART] New container: ${newContainerId.substring(0, 12)}`);
567
535
  // Recreate tmux session for sandbox
568
536
  const containerName = this.sandbox.getContainerName();
569
- const sessionName = getSessionName(this.agentName);
570
537
  // Build environment args for docker exec
571
538
  const envArgs = [];
572
539
  const allEnv = {
@@ -588,10 +555,15 @@ ${lastOutput}
588
555
  if (!created) {
589
556
  throw new Error("Failed to create tmux session for restarted sandbox");
590
557
  }
591
- // Update state with new container name
558
+ // Track new child PIDs and update state
559
+ const newChildPids = captureAgentChildPids(this.agentName);
592
560
  updateAgentInState(this.agentName, {
593
561
  sandboxContainer: containerName,
562
+ childPids: newChildPids,
594
563
  });
564
+ if (newChildPids.length > 0) {
565
+ console.log(`[RESTART] Tracking ${newChildPids.length} child PIDs: ${newChildPids.join(", ")}`);
566
+ }
595
567
  }
596
568
  else {
597
569
  // Non-sandbox restart — load saved session ID for native resume
@@ -615,6 +587,12 @@ ${lastOutput}
615
587
  AGENTMESH_AGENT_ID: this.agentId,
616
588
  ...this.runnerConfig.env,
617
589
  });
590
+ // Track new child PIDs
591
+ const newChildPids = captureAgentChildPids(this.agentName);
592
+ updateAgentInState(this.agentName, { childPids: newChildPids });
593
+ if (newChildPids.length > 0) {
594
+ console.log(`[RESTART] Tracking ${newChildPids.length} child PIDs: ${newChildPids.join(", ")}`);
595
+ }
618
596
  // Verify native resume and fallback if needed
619
597
  if (savedSessionId && savedContext) {
620
598
  const newSessionId = await waitForNewSessionId(this.agentName, preRestartSessionId, 15000);
@@ -709,27 +687,7 @@ ${lastOutput}
709
687
  async startServeMode() {
710
688
  console.log(`Starting opencode serve mode on port ${this.servePort}...`);
711
689
  const workdir = this.agentConfig.workdir || process.cwd();
712
- // Isolate OpenCode's SQLite database per agent to prevent WAL corruption.
713
- // See docs/RCA-OPENCODE-SQLITE-CORRUPTION.md for details.
714
- const agentDataDir = path.join(os.homedir(), ".agentmesh", "opencode-data", this.agentName);
715
- const agentOpencodeDir = path.join(agentDataDir, "opencode");
716
- if (!fs.existsSync(agentOpencodeDir)) {
717
- fs.mkdirSync(agentOpencodeDir, { recursive: true });
718
- }
719
- // Copy auth.json from default OpenCode data dir so agents inherit API keys.
720
- // Strips xAI provider to prevent OpenCode from defaulting to non-Anthropic models.
721
- const agentAuthPath = path.join(agentOpencodeDir, "auth.json");
722
- const sourceAuthPath = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
723
- if (!fs.existsSync(agentAuthPath) && fs.existsSync(sourceAuthPath)) {
724
- try {
725
- const auth = JSON.parse(fs.readFileSync(sourceAuthPath, "utf-8"));
726
- delete auth.xai;
727
- fs.writeFileSync(agentAuthPath, JSON.stringify(auth, null, 2));
728
- }
729
- catch {
730
- // Non-fatal — agent will just need manual auth
731
- }
732
- }
690
+ const agentDataDir = prepareOpenCodeRuntime(this.agentName);
733
691
  // Build environment for opencode serve
734
692
  const env = {
735
693
  ...process.env,
@@ -966,39 +924,22 @@ Logs: docker logs ${containerName}
966
924
  const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${repoAssignment.project.code.toLowerCase()}/${this.agentName}`;
967
925
  // If --auto-setup is enabled, automatically clone the repo
968
926
  if (this.autoSetup) {
969
- this.agentConfig.workdir = this.setupWorkspace(expandedPath, repo.url, repo.default_branch, repoAssignment.project.name);
927
+ this.agentConfig.workdir = setupWorkspace({
928
+ workspacePath: expandedPath,
929
+ repoUrl: repo.url,
930
+ defaultBranch: repo.default_branch,
931
+ projectName: repoAssignment.project.name,
932
+ });
970
933
  return;
971
934
  }
972
- console.error(`
973
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
974
- ⚠️ WORKDIR REQUIRED
975
-
976
- You have a project assignment with a repository, but no workdir is configured.
977
-
978
- Project: ${repoAssignment.project.name}
979
- Repo: ${repo.full_name}
980
- Branch: ${repo.default_branch}
981
-
982
- Option 1: Set workdir in project settings (recommended)
983
- - Go to AgentMesh HQ → Projects → ${repoAssignment.project.name} → Settings
984
- - Set the workdir field to the local path
985
-
986
- Option 2: Set up workspace manually and pass --workdir:
987
-
988
- mkdir -p ${suggestedPath}
989
- git clone ${repo.url} ${suggestedPath}
990
- cd ${suggestedPath} && git checkout ${repo.default_branch}
991
-
992
- Then start the agent with:
993
-
994
- agentmesh start -n ${this.agentName} --workdir ${suggestedPath}
995
-
996
- Option 3: Use --auto-setup to automatically clone the repository:
997
-
998
- agentmesh start -n ${this.agentName} --auto-setup
999
-
1000
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1001
- `);
935
+ console.error(renderMissingWorkdirMessage({
936
+ projectName: repoAssignment.project.name,
937
+ repoFullName: repo.full_name,
938
+ repoUrl: repo.url,
939
+ defaultBranch: repo.default_branch,
940
+ suggestedPath,
941
+ agentName: this.agentName,
942
+ }));
1002
943
  // No session to clean up - we haven't created it yet
1003
944
  process.exit(1);
1004
945
  }
@@ -1009,78 +950,16 @@ Option 3: Use --auto-setup to automatically clone the repository:
1009
950
  console.log("Could not fetch assignments:", error.message);
1010
951
  }
1011
952
  }
1012
- /**
1013
- * Sets up workspace by cloning repository or using existing clone
1014
- * Returns the absolute path to the workspace
1015
- */
1016
- setupWorkspace(workspacePath, repoUrl, defaultBranch, projectName) {
1017
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1018
- console.log(`🔧 AUTO-SETUP: Setting up workspace for ${projectName}`);
1019
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
1020
- // Check if directory already exists and is a git repo
1021
- const gitDir = path.join(workspacePath, ".git");
1022
- if (fs.existsSync(gitDir)) {
1023
- console.log(`✓ Workspace already exists: ${workspacePath}`);
1024
- console.log(` Updating from remote...`);
1025
- try {
1026
- // Fetch and checkout the branch
1027
- execSync(`git fetch origin`, { cwd: workspacePath, stdio: "inherit" });
1028
- execSync(`git checkout ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
1029
- execSync(`git pull origin ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
1030
- console.log(`✓ Workspace updated successfully\n`);
1031
- }
1032
- catch (error) {
1033
- console.warn(`⚠ Could not update workspace: ${error.message}`);
1034
- console.log(` Continuing with existing state...\n`);
1035
- }
1036
- return workspacePath;
1037
- }
1038
- // Create parent directories
1039
- const parentDir = path.dirname(workspacePath);
1040
- if (!fs.existsSync(parentDir)) {
1041
- console.log(`Creating directory: ${parentDir}`);
1042
- fs.mkdirSync(parentDir, { recursive: true });
1043
- }
1044
- // Clone the repository
1045
- console.log(`Cloning repository...`);
1046
- console.log(` URL: ${repoUrl}`);
1047
- console.log(` Path: ${workspacePath}`);
1048
- console.log(` Branch: ${defaultBranch}\n`);
1049
- try {
1050
- execSync(`git clone --branch ${defaultBranch} "${repoUrl}" "${workspacePath}"`, {
1051
- stdio: "inherit",
1052
- });
1053
- console.log(`\n✓ Repository cloned successfully`);
1054
- }
1055
- catch (error) {
1056
- console.error(`\n✗ Failed to clone repository: ${error.message}`);
1057
- console.error(`\nMake sure you have access to the repository and SSH keys are configured.`);
1058
- // No session to clean up - we haven't created it yet
1059
- process.exit(1);
1060
- }
1061
- console.log(`✓ Workspace ready: ${workspacePath}\n`);
1062
- return workspacePath;
1063
- }
1064
953
  /**
1065
954
  * Ensures the sandbox OpenCode config exists
1066
955
  * Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
1067
956
  */
1068
957
  ensureSandboxOpencodeConfig() {
1069
- const configDir = path.dirname(SANDBOX_OPENCODE_CONFIG_PATH);
1070
- if (!fs.existsSync(configDir)) {
1071
- fs.mkdirSync(configDir, { recursive: true });
1072
- }
1073
- // Build config with model if available
1074
- const config = {
1075
- ...SANDBOX_OPENCODE_CONFIG,
1076
- };
1077
- // Include model from runner config
1078
- const model = this.runnerConfig.env?.OPENCODE_MODEL;
1079
- if (model) {
1080
- config.model = model;
1081
- }
1082
- // Always write to ensure model is up to date
1083
- fs.writeFileSync(SANDBOX_OPENCODE_CONFIG_PATH, JSON.stringify(config, null, 2));
958
+ writeSandboxOpencodeConfig({
959
+ configPath: SANDBOX_OPENCODE_CONFIG_PATH,
960
+ baseConfig: SANDBOX_OPENCODE_CONFIG,
961
+ model: this.runnerConfig.env?.OPENCODE_MODEL,
962
+ });
1084
963
  console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
1085
964
  }
1086
965
  }