@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.
- package/README.md +39 -0
- package/dist/__tests__/context-template.test.d.ts +4 -0
- package/dist/__tests__/context-template.test.js +233 -0
- package/dist/__tests__/context-template.test.js.map +1 -0
- package/dist/__tests__/loader.test.js +140 -28
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/no-respawn.test.d.ts +1 -0
- package/dist/__tests__/no-respawn.test.js +254 -0
- package/dist/__tests__/no-respawn.test.js.map +1 -0
- package/dist/__tests__/onboard.test.d.ts +5 -0
- package/dist/__tests__/onboard.test.js +341 -0
- package/dist/__tests__/onboard.test.js.map +1 -0
- package/dist/__tests__/orphan-process.test.d.ts +11 -0
- package/dist/__tests__/orphan-process.test.js +286 -0
- package/dist/__tests__/orphan-process.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +16 -0
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/shared-resource-guards.test.d.ts +7 -0
- package/dist/__tests__/shared-resource-guards.test.js +260 -0
- package/dist/__tests__/shared-resource-guards.test.js.map +1 -0
- package/dist/__tests__/watchdog.test.js +138 -12
- package/dist/__tests__/watchdog.test.js.map +1 -1
- package/dist/cli/status.js +11 -0
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/stop.js +7 -2
- package/dist/cli/stop.js.map +1 -1
- package/dist/config/loader.d.ts +0 -4
- package/dist/config/loader.js +102 -42
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +6 -4
- package/dist/core/daemon/assignment-message.d.ts +12 -0
- package/dist/core/daemon/assignment-message.js +36 -0
- package/dist/core/daemon/assignment-message.js.map +1 -0
- package/dist/core/daemon/bootstrap.d.ts +35 -0
- package/dist/core/daemon/bootstrap.js +52 -0
- package/dist/core/daemon/bootstrap.js.map +1 -0
- package/dist/core/daemon/context-template.d.ts +11 -0
- package/dist/core/daemon/context-template.js +144 -0
- package/dist/core/daemon/context-template.js.map +1 -0
- package/dist/core/daemon/crash-log.d.ts +14 -0
- package/dist/core/daemon/crash-log.js +23 -0
- package/dist/core/daemon/crash-log.js.map +1 -0
- package/dist/core/daemon/git-auth.d.ts +18 -0
- package/dist/core/daemon/git-auth.js +88 -0
- package/dist/core/daemon/git-auth.js.map +1 -0
- package/dist/core/daemon/health-policy.d.ts +17 -0
- package/dist/core/daemon/health-policy.js +24 -0
- package/dist/core/daemon/health-policy.js.map +1 -0
- package/dist/core/daemon/sandbox-config.d.ts +9 -0
- package/dist/core/daemon/sandbox-config.js +17 -0
- package/dist/core/daemon/sandbox-config.js.map +1 -0
- package/dist/core/daemon/state.d.ts +33 -0
- package/dist/core/daemon/state.js +78 -0
- package/dist/core/daemon/state.js.map +1 -0
- package/dist/core/daemon/tmux-session.d.ts +17 -0
- package/dist/core/daemon/tmux-session.js +34 -0
- package/dist/core/daemon/tmux-session.js.map +1 -0
- package/dist/core/daemon/workspace.d.ts +23 -0
- package/dist/core/daemon/workspace.js +90 -0
- package/dist/core/daemon/workspace.js.map +1 -0
- package/dist/core/daemon.d.ts +9 -12
- package/dist/core/daemon.js +293 -393
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.d.ts +5 -1
- package/dist/core/injector.js +83 -0
- package/dist/core/injector.js.map +1 -1
- package/dist/core/registry.d.ts +62 -0
- package/dist/core/registry.js +18 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner/build.d.ts +9 -0
- package/dist/core/runner/build.js +53 -0
- package/dist/core/runner/build.js.map +1 -0
- package/dist/core/runner/detect.d.ts +5 -0
- package/dist/core/runner/detect.js +14 -0
- package/dist/core/runner/detect.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +5 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/model.d.ts +5 -0
- package/dist/core/runner/model.js +7 -0
- package/dist/core/runner/model.js.map +1 -0
- package/dist/core/runner/opencode-models.d.ts +15 -0
- package/dist/core/runner/opencode-models.js +70 -0
- package/dist/core/runner/opencode-models.js.map +1 -0
- package/dist/core/runner/types.d.ts +19 -0
- package/dist/core/runner/types.js +8 -0
- package/dist/core/runner/types.js.map +1 -0
- package/dist/core/runner.d.ts +5 -47
- package/dist/core/runner.js +5 -167
- package/dist/core/runner.js.map +1 -1
- package/dist/core/tmux-runtime.d.ts +13 -0
- package/dist/core/tmux-runtime.js +72 -0
- package/dist/core/tmux-runtime.js.map +1 -0
- package/dist/core/tmux.d.ts +7 -1
- package/dist/core/tmux.js +75 -45
- package/dist/core/tmux.js.map +1 -1
- package/dist/core/watchdog.d.ts +18 -1
- package/dist/core/watchdog.js +78 -29
- package/dist/core/watchdog.js.map +1 -1
- package/package.json +24 -4
- package/src/__tests__/context.test.ts +0 -464
- package/src/__tests__/injector.test.ts +0 -29
- package/src/__tests__/jwt.test.ts +0 -112
- package/src/__tests__/loader.test.ts +0 -239
- package/src/__tests__/runner.test.ts +0 -104
- package/src/__tests__/sandbox.test.ts +0 -435
- package/src/__tests__/watchdog.test.ts +0 -368
- package/src/cli/attach.ts +0 -22
- package/src/cli/build.ts +0 -145
- package/src/cli/config.ts +0 -148
- package/src/cli/context.ts +0 -231
- package/src/cli/deploy.ts +0 -155
- package/src/cli/index.ts +0 -376
- package/src/cli/init.ts +0 -75
- package/src/cli/list.ts +0 -70
- package/src/cli/local.ts +0 -183
- package/src/cli/logs.ts +0 -64
- package/src/cli/migrate.ts +0 -212
- package/src/cli/nudge.ts +0 -81
- package/src/cli/restart.ts +0 -59
- package/src/cli/slack.ts +0 -70
- package/src/cli/start.ts +0 -118
- package/src/cli/status.ts +0 -91
- package/src/cli/stop.ts +0 -48
- package/src/cli/test.ts +0 -143
- package/src/cli/token.ts +0 -188
- package/src/cli/whoami.ts +0 -142
- package/src/config/loader.ts +0 -121
- package/src/config/schema.ts +0 -68
- package/src/context/handoff.ts +0 -122
- package/src/context/index.ts +0 -8
- package/src/context/schema.ts +0 -111
- package/src/context/storage.ts +0 -197
- package/src/core/daemon.ts +0 -1317
- package/src/core/heartbeat.ts +0 -129
- package/src/core/injector.ts +0 -292
- package/src/core/registry.ts +0 -159
- package/src/core/runner.ts +0 -225
- package/src/core/sandbox.ts +0 -547
- package/src/core/session-id.ts +0 -111
- package/src/core/tmux.ts +0 -405
- package/src/core/watchdog.ts +0 -238
- package/src/core/websocket.ts +0 -94
- package/src/index.ts +0 -10
- package/src/utils/jwt.ts +0 -87
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -12
package/dist/core/daemon.js
CHANGED
|
@@ -2,22 +2,29 @@ import { execSync, 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 {
|
|
5
|
+
import { getAgentState, loadState, 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 { removeClaudeMd, writeClaudeMd } from "./daemon/context-template.js";
|
|
10
|
+
import { formatCrashLog } from "./daemon/crash-log.js";
|
|
11
|
+
import { cleanupGitAuth, setupGitAuth } from "./daemon/git-auth.js";
|
|
12
|
+
import { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, } from "./daemon/health-policy.js";
|
|
13
|
+
import { writeSandboxOpencodeConfig } from "./daemon/sandbox-config.js";
|
|
14
|
+
import { captureAgentChildPids, persistRunningState } from "./daemon/state.js";
|
|
15
|
+
import { startTmuxRuntimeSession } from "./daemon/tmux-session.js";
|
|
16
|
+
import { configureGitIdentity, setupWorkspace, validatePushAccess } from "./daemon/workspace.js";
|
|
7
17
|
import { Heartbeat } from "./heartbeat.js";
|
|
8
|
-
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
9
|
-
import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
|
|
10
|
-
import {
|
|
18
|
+
import { handleWebSocketEvent, injectOnboardMessage, injectRestoredContext, injectStartupMessage, } from "./injector.js";
|
|
19
|
+
import { checkInbox, fetchAssignments, fetchOnboard, registerAgent, } from "./registry.js";
|
|
20
|
+
import { getRunnerDisplayName } from "./runner.js";
|
|
11
21
|
import { DockerSandbox } from "./sandbox.js";
|
|
12
|
-
import { getLatestSessionId,
|
|
13
|
-
import { captureSessionContext, captureSessionOutput,
|
|
22
|
+
import { getLatestSessionId, waitForNewSessionId } from "./session-id.js";
|
|
23
|
+
import { captureSessionContext, captureSessionOutput, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
|
|
24
|
+
import { prepareOpenCodeRuntime } from "./tmux-runtime.js";
|
|
14
25
|
import { checkAgentProgress, cleanupOrphanContainers, isProcessRunning, sendNudge, } from "./watchdog.js";
|
|
15
26
|
import { AgentWebSocket } from "./websocket.js";
|
|
16
|
-
//
|
|
17
|
-
const MAX_RESTART_ATTEMPTS = 3;
|
|
18
|
-
// Time after which restart count resets (30 minutes of stable operation)
|
|
19
|
-
const RESTART_COUNT_RESET_MS = 30 * 60 * 1000;
|
|
20
|
-
// Time to wait after nudging before restarting (2 minutes)
|
|
27
|
+
// Time to wait after nudging before marking as stuck (2 minutes)
|
|
21
28
|
const NUDGE_WAIT_MS = 2 * 60 * 1000;
|
|
22
29
|
// Path to the sandbox OpenCode config (permissive permissions)
|
|
23
30
|
const SANDBOX_OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".agentmesh", "opencode-sandbox.json");
|
|
@@ -47,63 +54,30 @@ export class AgentDaemon {
|
|
|
47
54
|
sandboxImage;
|
|
48
55
|
sandboxCpu;
|
|
49
56
|
sandboxMemory;
|
|
57
|
+
onboardData = null;
|
|
50
58
|
sandbox = null;
|
|
51
59
|
healthCheckInterval = null;
|
|
52
|
-
serverContext;
|
|
53
60
|
// Session resume tracking
|
|
54
61
|
_preStartSessionId;
|
|
55
62
|
_attemptedResumeSessionId;
|
|
56
|
-
//
|
|
57
|
-
restartCount = 0;
|
|
58
|
-
lastStableTime = null;
|
|
63
|
+
// Stuck detection tracking
|
|
59
64
|
stuckSince = null;
|
|
60
65
|
nudgeSentAt = null;
|
|
61
66
|
constructor(options) {
|
|
62
|
-
const
|
|
63
|
-
|
|
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;
|
|
67
|
+
const boot = bootstrapDaemon(options);
|
|
68
|
+
this.config = boot.config;
|
|
72
69
|
this.agentName = options.name;
|
|
73
|
-
this.shouldRestoreContext =
|
|
74
|
-
this.isWorkerAgent =
|
|
75
|
-
this.autoSetup =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
});
|
|
70
|
+
this.shouldRestoreContext = boot.shouldRestoreContext;
|
|
71
|
+
this.isWorkerAgent = boot.isWorkerAgent;
|
|
72
|
+
this.autoSetup = boot.autoSetup;
|
|
73
|
+
this.agentConfig = boot.agentConfig;
|
|
74
|
+
this.serveMode = boot.serveMode;
|
|
75
|
+
this.servePort = boot.servePort;
|
|
76
|
+
this.sandboxMode = boot.sandboxMode;
|
|
77
|
+
this.sandboxImage = boot.sandboxImage;
|
|
78
|
+
this.sandboxCpu = boot.sandboxCpu;
|
|
79
|
+
this.sandboxMemory = boot.sandboxMemory;
|
|
80
|
+
this.runnerConfig = boot.runnerConfig;
|
|
107
81
|
const runnerName = getRunnerDisplayName(this.runnerConfig.type);
|
|
108
82
|
console.log(`Runner: ${runnerName}`);
|
|
109
83
|
console.log(`Effective model: ${this.runnerConfig.model}`);
|
|
@@ -121,8 +95,13 @@ export class AgentDaemon {
|
|
|
121
95
|
throw new Error(`Agent "${this.agentName}" is already running (PID: ${existingState.pid}). ` +
|
|
122
96
|
`Use 'agentmesh stop ${this.agentName}' first.`);
|
|
123
97
|
}
|
|
124
|
-
// Process not running
|
|
98
|
+
// Process not running — clean up stale state and any orphaned child processes
|
|
125
99
|
console.log(`Cleaning up stale state for PID ${existingState.pid}`);
|
|
100
|
+
const orphanPids = existingState.childPids ?? [];
|
|
101
|
+
if (orphanPids.length > 0) {
|
|
102
|
+
console.log(`[STARTUP] Found ${orphanPids.length} orphaned child PIDs from previous run — killing...`);
|
|
103
|
+
killProcessTree(orphanPids);
|
|
104
|
+
}
|
|
126
105
|
}
|
|
127
106
|
// Clean up orphan containers in sandbox mode
|
|
128
107
|
if (this.sandboxMode) {
|
|
@@ -131,9 +110,6 @@ export class AgentDaemon {
|
|
|
131
110
|
console.log(`Cleaned up ${cleaned} orphan container(s)`);
|
|
132
111
|
}
|
|
133
112
|
}
|
|
134
|
-
// Reset restart count on manual start
|
|
135
|
-
this.restartCount = 0;
|
|
136
|
-
this.lastStableTime = new Date();
|
|
137
113
|
// Register with hub first (needed for assignment check)
|
|
138
114
|
console.log("Registering with AgentMesh hub...");
|
|
139
115
|
console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
|
|
@@ -151,15 +127,71 @@ export class AgentDaemon {
|
|
|
151
127
|
if (registration.status === "re-registered") {
|
|
152
128
|
console.log(`Re-registered as: ${this.agentId}`);
|
|
153
129
|
if (registration.context && Object.keys(registration.context).length > 0) {
|
|
154
|
-
this.serverContext = registration.context;
|
|
155
130
|
console.log(`Server context restored: ${Object.keys(registration.context).join(", ")}`);
|
|
156
131
|
}
|
|
157
132
|
}
|
|
158
133
|
else {
|
|
159
134
|
console.log(`Registered as: ${this.agentId}`);
|
|
160
135
|
}
|
|
136
|
+
// Fetch onboard payload (project context, credentials, team)
|
|
137
|
+
try {
|
|
138
|
+
this.onboardData = await fetchOnboard(this.config.hubUrl, this.token);
|
|
139
|
+
if (this.onboardData) {
|
|
140
|
+
console.log(`Onboard data received${this.onboardData.project ? `: project=${this.onboardData.project.name}` : ""}`);
|
|
141
|
+
// Set up git credentials before workspace setup
|
|
142
|
+
for (const repo of this.onboardData.repos) {
|
|
143
|
+
if (repo.credential) {
|
|
144
|
+
const extraEnv = setupGitAuth({
|
|
145
|
+
type: repo.credential.type,
|
|
146
|
+
value: repo.credential.value,
|
|
147
|
+
repoUrl: repo.url,
|
|
148
|
+
agentName: this.agentName,
|
|
149
|
+
});
|
|
150
|
+
Object.assign(this.runnerConfig.env, extraEnv);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Cache onboard data locally for offline fallback
|
|
154
|
+
this.cacheOnboardData();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.log("Onboard fetch failed (non-fatal):", error.message);
|
|
159
|
+
// Try loading cached onboard data
|
|
160
|
+
this.onboardData = this.loadCachedOnboardData();
|
|
161
|
+
if (this.onboardData) {
|
|
162
|
+
console.log("Loaded cached onboard data");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
161
165
|
// Check assignments and auto-setup workdir if needed (before creating tmux session)
|
|
162
166
|
await this.checkAssignments();
|
|
167
|
+
// Enforce unique working directories — no two running agents may share the same workdir
|
|
168
|
+
if (this.agentConfig.workdir) {
|
|
169
|
+
const resolvedWorkdir = path.resolve(this.agentConfig.workdir);
|
|
170
|
+
const state = loadState();
|
|
171
|
+
const conflict = state.agents.find((a) => a.name !== this.agentName &&
|
|
172
|
+
a.workdir &&
|
|
173
|
+
path.resolve(a.workdir) === resolvedWorkdir &&
|
|
174
|
+
a.pid > 0 &&
|
|
175
|
+
isProcessRunning(a.pid));
|
|
176
|
+
if (conflict) {
|
|
177
|
+
throw new Error(`Workdir conflict: "${resolvedWorkdir}" is already in use by agent "${conflict.name}" (PID: ${conflict.pid}).\n` +
|
|
178
|
+
`Each agent must have its own working directory to avoid git conflicts.\n` +
|
|
179
|
+
`Use --workdir to specify a unique path, e.g.:\n` +
|
|
180
|
+
` agentmesh start --name ${this.agentName} --workdir ~/Dev/${this.assignedProject || "project"}-${this.agentName}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Check for serve port collision
|
|
184
|
+
if (this.serveMode) {
|
|
185
|
+
const state = loadState();
|
|
186
|
+
const portConflict = state.agents.find((a) => a.name !== this.agentName &&
|
|
187
|
+
a.servePort === this.servePort &&
|
|
188
|
+
a.pid > 0 &&
|
|
189
|
+
isProcessRunning(a.pid));
|
|
190
|
+
if (portConflict) {
|
|
191
|
+
throw new Error(`Port ${this.servePort} already in use by agent "${portConflict.name}" (PID: ${portConflict.pid}).\n` +
|
|
192
|
+
`Use --serve-port to specify a different port.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
163
195
|
// Choose runtime mode: sandbox > serve > tmux
|
|
164
196
|
if (this.sandboxMode) {
|
|
165
197
|
await this.startSandboxMode();
|
|
@@ -168,39 +200,16 @@ export class AgentDaemon {
|
|
|
168
200
|
await this.startServeMode();
|
|
169
201
|
}
|
|
170
202
|
else {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
}
|
|
203
|
+
const sessionStart = startTmuxRuntimeSession({
|
|
204
|
+
agentName: this.agentName,
|
|
205
|
+
agentId: this.agentId,
|
|
206
|
+
command: this.agentConfig.command,
|
|
207
|
+
workdir: this.agentConfig.workdir,
|
|
208
|
+
runnerEnv: this.runnerConfig.env,
|
|
209
|
+
shouldRestoreContext: this.shouldRestoreContext,
|
|
210
|
+
});
|
|
211
|
+
this._preStartSessionId = sessionStart.preStartSessionId;
|
|
212
|
+
this._attemptedResumeSessionId = sessionStart.attemptedResumeSessionId;
|
|
204
213
|
// Inject environment variables into tmux session
|
|
205
214
|
console.log("Injecting environment variables...");
|
|
206
215
|
updateSessionEnvironment(this.agentName, {
|
|
@@ -209,20 +218,27 @@ export class AgentDaemon {
|
|
|
209
218
|
});
|
|
210
219
|
}
|
|
211
220
|
// Save state including runtime model info
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
name: this.agentName,
|
|
221
|
+
persistRunningState({
|
|
222
|
+
agentName: this.agentName,
|
|
215
223
|
agentId: this.agentId,
|
|
216
224
|
pid: process.pid,
|
|
217
|
-
tmuxSession: sessionName,
|
|
218
|
-
startedAt: new Date().toISOString(),
|
|
219
225
|
token: this.token,
|
|
220
226
|
workdir: this.agentConfig.workdir,
|
|
221
227
|
assignedProject: this.assignedProject,
|
|
222
228
|
runtimeModel: this.runnerConfig.model,
|
|
223
229
|
runnerType: this.runnerConfig.type,
|
|
224
230
|
sandboxContainer: this.sandbox?.getContainerName(),
|
|
231
|
+
serveMode: this.serveMode,
|
|
232
|
+
servePort: this.servePort,
|
|
225
233
|
});
|
|
234
|
+
// Track child PIDs for cleanup on restart/stop (tmux mode only — sandbox/serve manage their own)
|
|
235
|
+
if (!this.sandboxMode && !this.serveMode) {
|
|
236
|
+
const childPids = captureAgentChildPids(this.agentName);
|
|
237
|
+
if (childPids.length > 0) {
|
|
238
|
+
updateAgentInState(this.agentName, { childPids });
|
|
239
|
+
console.log(`[STARTUP] Tracking ${childPids.length} child PIDs: ${childPids.join(", ")}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
226
242
|
// Start heartbeat with auto-refresh
|
|
227
243
|
console.log("Starting heartbeat...");
|
|
228
244
|
this.heartbeat = new Heartbeat({
|
|
@@ -312,6 +328,20 @@ export class AgentDaemon {
|
|
|
312
328
|
console.error("Failed to check inbox:", error);
|
|
313
329
|
injectStartupMessage(this.agentName, 0);
|
|
314
330
|
}
|
|
331
|
+
// Inject onboard project context
|
|
332
|
+
if (this.onboardData?.project) {
|
|
333
|
+
injectOnboardMessage(this.agentName, this.onboardData);
|
|
334
|
+
}
|
|
335
|
+
// Write persistent CLAUDE.md context file
|
|
336
|
+
if (this.onboardData && this.agentConfig.workdir) {
|
|
337
|
+
const mdPath = writeClaudeMd({
|
|
338
|
+
workdir: this.agentConfig.workdir,
|
|
339
|
+
onboard: this.onboardData,
|
|
340
|
+
});
|
|
341
|
+
if (mdPath) {
|
|
342
|
+
console.log(`✓ CLAUDE.md written: ${mdPath}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
315
345
|
// Restore context from previous session
|
|
316
346
|
if (this.shouldRestoreContext && this.agentId) {
|
|
317
347
|
console.log("Checking for previous context...");
|
|
@@ -403,27 +433,28 @@ Nudge agent:
|
|
|
403
433
|
this.healthCheckInterval = setInterval(async () => {
|
|
404
434
|
if (!this.isRunning)
|
|
405
435
|
return;
|
|
406
|
-
// 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
|
-
}
|
|
414
|
-
}
|
|
415
436
|
// For sandbox mode, pass container name so health check looks inside container
|
|
416
437
|
const containerName = this.sandboxMode ? this.sandbox?.getContainerName() : undefined;
|
|
417
438
|
const health = isSessionHealthy(this.agentName, containerName);
|
|
418
439
|
if (!health.healthy) {
|
|
419
|
-
// Session died -
|
|
440
|
+
// Session died - log and mark as failed (no auto-restart)
|
|
420
441
|
await this.handleSessionDeath(health.reason || "unknown", logDir);
|
|
421
442
|
return;
|
|
422
443
|
}
|
|
423
444
|
// Session is alive - check progress watchdog
|
|
424
445
|
const progress = checkAgentProgress(this.agentName, containerName);
|
|
425
|
-
if (progress.status === "
|
|
426
|
-
|
|
446
|
+
if (progress.status === "waiting_for_human") {
|
|
447
|
+
// Agent is intentionally waiting for human input - do not classify as stuck
|
|
448
|
+
if (this.stuckSince) {
|
|
449
|
+
// Clear any prior stuck tracking since the agent signalled a legitimate wait
|
|
450
|
+
this.stuckSince = null;
|
|
451
|
+
this.nudgeSentAt = null;
|
|
452
|
+
updateAgentInState(this.agentName, { stuckSince: undefined, status: "waiting" });
|
|
453
|
+
}
|
|
454
|
+
console.log(`[HEALTH] Agent is waiting for human input: ${progress.details}`);
|
|
455
|
+
}
|
|
456
|
+
else if (progress.status === "permission_blocked" || progress.status === "stuck") {
|
|
457
|
+
this.handleStuckAgent(progress);
|
|
427
458
|
}
|
|
428
459
|
else if (progress.status === "active") {
|
|
429
460
|
// Agent is working - reset stuck tracking
|
|
@@ -433,7 +464,6 @@ Nudge agent:
|
|
|
433
464
|
this.nudgeSentAt = null;
|
|
434
465
|
updateAgentInState(this.agentName, { stuckSince: undefined, status: "running" });
|
|
435
466
|
}
|
|
436
|
-
this.lastStableTime = new Date();
|
|
437
467
|
}
|
|
438
468
|
}, 60000); // Check every 60 seconds
|
|
439
469
|
}
|
|
@@ -451,86 +481,55 @@ Nudge agent:
|
|
|
451
481
|
catch {
|
|
452
482
|
lastOutput = "Failed to capture session output";
|
|
453
483
|
}
|
|
454
|
-
const crashLog =
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
Workdir: ${this.agentConfig.workdir}
|
|
465
|
-
Model: ${this.runnerConfig.model}
|
|
466
|
-
|
|
467
|
-
--- Last Session Output ---
|
|
468
|
-
${lastOutput}
|
|
469
|
-
================================================================================
|
|
470
|
-
|
|
471
|
-
`;
|
|
484
|
+
const crashLog = formatCrashLog({
|
|
485
|
+
timestamp,
|
|
486
|
+
agentName: this.agentName,
|
|
487
|
+
agentId: this.agentId,
|
|
488
|
+
reason,
|
|
489
|
+
sandboxLabel: this.sandboxMode ? this.sandbox?.getContainerName() || "sandbox" : "none",
|
|
490
|
+
workdir: this.agentConfig.workdir,
|
|
491
|
+
model: this.runnerConfig.model,
|
|
492
|
+
lastOutput,
|
|
493
|
+
});
|
|
472
494
|
fs.appendFileSync(logFile, crashLog);
|
|
473
|
-
// Save context
|
|
495
|
+
// Save context before marking as failed
|
|
474
496
|
if (this.agentId) {
|
|
475
497
|
this.saveAgentContext();
|
|
476
498
|
}
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this.lastStableTime = new Date();
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
console.error(`[RESTART] Failed to restart: ${error.message}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
// Exceeded restart limit - mark as failed
|
|
497
|
-
console.error(`[FAILED] Agent exceeded restart limit (${MAX_RESTART_ATTEMPTS}). Manual intervention required.`);
|
|
498
|
-
// Terminal bell to alert user
|
|
499
|
-
process.stdout.write("\x07");
|
|
500
|
-
updateAgentInState(this.agentName, {
|
|
501
|
-
status: "failed",
|
|
502
|
-
restartCount: this.restartCount,
|
|
503
|
-
});
|
|
504
|
-
// Stop monitoring
|
|
505
|
-
this.isRunning = false;
|
|
506
|
-
if (this.healthCheckInterval) {
|
|
507
|
-
clearInterval(this.healthCheckInterval);
|
|
508
|
-
this.healthCheckInterval = null;
|
|
509
|
-
}
|
|
499
|
+
// Mark as failed — no auto-restart, user must intervene
|
|
500
|
+
console.error(`[FAILED] Session died: ${reason}. Manual intervention required (use 'agentmesh restart ${this.agentName}').`);
|
|
501
|
+
// Terminal bell to alert user
|
|
502
|
+
process.stdout.write("\x07");
|
|
503
|
+
updateAgentInState(this.agentName, {
|
|
504
|
+
status: "failed",
|
|
505
|
+
});
|
|
506
|
+
// Stop monitoring
|
|
507
|
+
this.isRunning = false;
|
|
508
|
+
if (this.healthCheckInterval) {
|
|
509
|
+
clearInterval(this.healthCheckInterval);
|
|
510
|
+
this.healthCheckInterval = null;
|
|
510
511
|
}
|
|
511
512
|
}
|
|
512
513
|
/**
|
|
513
514
|
* Handles stuck agent - sends nudge first, then restarts if still stuck
|
|
514
515
|
*/
|
|
515
|
-
|
|
516
|
+
handleStuckAgent(progress) {
|
|
516
517
|
const now = new Date();
|
|
517
518
|
if (!this.stuckSince) {
|
|
518
519
|
// First detection of stuck state
|
|
519
520
|
this.stuckSince = now;
|
|
520
|
-
console.log(`[HEALTH] Agent appears stuck: ${progress
|
|
521
|
+
console.log(`[HEALTH] Agent appears stuck: ${getStuckDetail(progress)}`);
|
|
521
522
|
updateAgentInState(this.agentName, {
|
|
522
523
|
stuckSince: now.toISOString(),
|
|
523
524
|
status: "stuck",
|
|
524
525
|
});
|
|
525
526
|
}
|
|
526
|
-
//
|
|
527
|
+
// Nudge worker agents — don't escalate to restart
|
|
527
528
|
if (this.isWorkerAgent) {
|
|
528
529
|
// If we haven't sent a nudge yet, send one
|
|
529
530
|
if (!this.nudgeSentAt) {
|
|
530
531
|
console.log(`[HEALTH] Sending nudge to worker agent...`);
|
|
531
|
-
const nudgeMessage = progress
|
|
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.";
|
|
532
|
+
const nudgeMessage = getNudgeMessage(progress);
|
|
534
533
|
const sent = sendNudge(this.agentName, nudgeMessage);
|
|
535
534
|
if (sent) {
|
|
536
535
|
this.nudgeSentAt = now;
|
|
@@ -542,114 +541,13 @@ ${lastOutput}
|
|
|
542
541
|
return;
|
|
543
542
|
}
|
|
544
543
|
// Check if enough time has passed since nudge
|
|
545
|
-
|
|
546
|
-
if (timeSinceNudge < NUDGE_WAIT_MS) {
|
|
544
|
+
if (isWithinNudgeWaitWindow(this.nudgeSentAt, NUDGE_WAIT_MS, now)) {
|
|
547
545
|
// Still waiting for agent to respond to nudge
|
|
548
546
|
return;
|
|
549
547
|
}
|
|
548
|
+
// Nudge grace period expired — log warning but do NOT restart
|
|
549
|
+
console.log(`[HEALTH] Agent still stuck after nudge. Manual intervention required.`);
|
|
550
550
|
}
|
|
551
|
-
// Agent still stuck - trigger restart (or restart immediately if not a worker)
|
|
552
|
-
console.log(`[HEALTH] Agent still stuck${this.isWorkerAgent ? " after nudge" : ""}, triggering restart...`);
|
|
553
|
-
this.stuckSince = null;
|
|
554
|
-
this.nudgeSentAt = null;
|
|
555
|
-
await this.handleSessionDeath("stuck_after_nudge", path.join(os.homedir(), ".agentmesh", "logs"));
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Restarts the agent session (sandbox or non-sandbox)
|
|
559
|
-
*/
|
|
560
|
-
async restartSession() {
|
|
561
|
-
// Destroy existing session
|
|
562
|
-
destroySession(this.agentName);
|
|
563
|
-
if (this.sandboxMode && this.sandbox) {
|
|
564
|
-
// Restart sandbox container
|
|
565
|
-
const newContainerId = await this.sandbox.restart();
|
|
566
|
-
console.log(`[RESTART] New container: ${newContainerId.substring(0, 12)}`);
|
|
567
|
-
// Recreate tmux session for sandbox
|
|
568
|
-
const containerName = this.sandbox.getContainerName();
|
|
569
|
-
const sessionName = getSessionName(this.agentName);
|
|
570
|
-
// Build environment args for docker exec
|
|
571
|
-
const envArgs = [];
|
|
572
|
-
const allEnv = {
|
|
573
|
-
...this.runnerConfig.env,
|
|
574
|
-
AGENT_TOKEN: this.token,
|
|
575
|
-
AGENTMESH_AGENT_ID: this.agentId,
|
|
576
|
-
};
|
|
577
|
-
for (const [key, value] of Object.entries(allEnv)) {
|
|
578
|
-
if (value !== undefined && value !== "") {
|
|
579
|
-
envArgs.push(`-e "${key}=${value}"`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
const envString = envArgs.join(" ");
|
|
583
|
-
const modelArg = this.runnerConfig.env?.OPENCODE_MODEL
|
|
584
|
-
? ` --model ${this.runnerConfig.env.OPENCODE_MODEL}`
|
|
585
|
-
: "";
|
|
586
|
-
const dockerExecCommand = `docker exec -it ${envString} ${containerName} opencode${modelArg}`;
|
|
587
|
-
const created = createSession(this.agentName, dockerExecCommand, undefined, undefined);
|
|
588
|
-
if (!created) {
|
|
589
|
-
throw new Error("Failed to create tmux session for restarted sandbox");
|
|
590
|
-
}
|
|
591
|
-
// Update state with new container name
|
|
592
|
-
updateAgentInState(this.agentName, {
|
|
593
|
-
sandboxContainer: containerName,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
// Non-sandbox restart — load saved session ID for native resume
|
|
598
|
-
let savedSessionId;
|
|
599
|
-
let savedContext = null;
|
|
600
|
-
if (this.agentId) {
|
|
601
|
-
savedContext = loadContext(this.agentId);
|
|
602
|
-
savedSessionId = savedContext?.custom?.opencodeSessionId;
|
|
603
|
-
if (savedSessionId) {
|
|
604
|
-
console.log(`[RESTART] Attempting to resume OpenCode session: ${savedSessionId}`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
const preRestartSessionId = snapshotSessionId(this.agentName);
|
|
608
|
-
const created = createSession(this.agentName, this.agentConfig.command, this.agentConfig.workdir, this.runnerConfig.env, savedSessionId);
|
|
609
|
-
if (!created) {
|
|
610
|
-
throw new Error("Failed to create tmux session");
|
|
611
|
-
}
|
|
612
|
-
// Re-inject environment
|
|
613
|
-
updateSessionEnvironment(this.agentName, {
|
|
614
|
-
AGENT_TOKEN: this.token,
|
|
615
|
-
AGENTMESH_AGENT_ID: this.agentId,
|
|
616
|
-
...this.runnerConfig.env,
|
|
617
|
-
});
|
|
618
|
-
// Verify native resume and fallback if needed
|
|
619
|
-
if (savedSessionId && savedContext) {
|
|
620
|
-
const newSessionId = await waitForNewSessionId(this.agentName, preRestartSessionId, 15000);
|
|
621
|
-
if (!newSessionId) {
|
|
622
|
-
const health = isSessionHealthy(this.agentName);
|
|
623
|
-
const currentSessionId = getLatestSessionId(this.agentName);
|
|
624
|
-
if (!health.healthy) {
|
|
625
|
-
console.log(`[RESTART] Fallback: OpenCode not healthy, injecting text summary.`);
|
|
626
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
627
|
-
saveContext(savedContext);
|
|
628
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
629
|
-
}
|
|
630
|
-
else if (currentSessionId === savedSessionId) {
|
|
631
|
-
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
console.log(`[RESTART] Fallback: session not found. Injecting text summary.`);
|
|
635
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
636
|
-
saveContext(savedContext);
|
|
637
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
else if (newSessionId === savedSessionId) {
|
|
641
|
-
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
console.log(`[RESTART] Fallback: resume failed (got ${newSessionId}). Injecting text summary.`);
|
|
645
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: newSessionId };
|
|
646
|
-
saveContext(savedContext);
|
|
647
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
// Wait for session to be ready
|
|
652
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
653
551
|
}
|
|
654
552
|
async stop() {
|
|
655
553
|
console.log(`\nStopping agent: ${this.agentName}`);
|
|
@@ -664,6 +562,12 @@ ${lastOutput}
|
|
|
664
562
|
console.log("Saving agent context...");
|
|
665
563
|
this.saveAgentContext();
|
|
666
564
|
}
|
|
565
|
+
// Clean up git credential files
|
|
566
|
+
cleanupGitAuth(this.agentName);
|
|
567
|
+
// Remove CLAUDE.md context file
|
|
568
|
+
if (this.agentConfig.workdir) {
|
|
569
|
+
removeClaudeMd(this.agentConfig.workdir);
|
|
570
|
+
}
|
|
667
571
|
// Stop heartbeat
|
|
668
572
|
if (this.heartbeat) {
|
|
669
573
|
this.heartbeat.stop();
|
|
@@ -709,27 +613,7 @@ ${lastOutput}
|
|
|
709
613
|
async startServeMode() {
|
|
710
614
|
console.log(`Starting opencode serve mode on port ${this.servePort}...`);
|
|
711
615
|
const workdir = this.agentConfig.workdir || process.cwd();
|
|
712
|
-
|
|
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
|
-
}
|
|
616
|
+
const agentDataDir = prepareOpenCodeRuntime(this.agentName);
|
|
733
617
|
// Build environment for opencode serve
|
|
734
618
|
const env = {
|
|
735
619
|
...process.env,
|
|
@@ -954,8 +838,51 @@ Logs: docker logs ${containerName}
|
|
|
954
838
|
if (!this.agentConfig.workdir) {
|
|
955
839
|
const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
|
|
956
840
|
if (assignmentWithWorkdir?.project.workdir) {
|
|
957
|
-
|
|
958
|
-
|
|
841
|
+
// Append agent name as sibling directory to avoid shared workdir conflicts
|
|
842
|
+
// e.g. /Users/raju/Dev/agentmesh -> /Users/raju/Dev/agentmesh-agent-1
|
|
843
|
+
const baseWorkdir = assignmentWithWorkdir.project.workdir;
|
|
844
|
+
const uniqueWorkdir = path.join(path.dirname(baseWorkdir), `${path.basename(baseWorkdir)}-${this.agentName}`);
|
|
845
|
+
console.log(`Using workdir from project settings (per-agent): ${uniqueWorkdir}`);
|
|
846
|
+
// If the unique dir doesn't exist, set up workspace (clone or pull)
|
|
847
|
+
const gitDir = path.join(uniqueWorkdir, ".git");
|
|
848
|
+
if (!fs.existsSync(gitDir)) {
|
|
849
|
+
// Find repo info from assignments or onboard data for cloning
|
|
850
|
+
const repoAssignment = assignments.find((a) => a.repo !== null);
|
|
851
|
+
if (repoAssignment?.repo) {
|
|
852
|
+
this.agentConfig.workdir = setupWorkspace({
|
|
853
|
+
workspacePath: uniqueWorkdir,
|
|
854
|
+
repoUrl: repoAssignment.repo.url,
|
|
855
|
+
defaultBranch: repoAssignment.repo.default_branch,
|
|
856
|
+
projectName: repoAssignment.project.name,
|
|
857
|
+
});
|
|
858
|
+
configureGitIdentity({
|
|
859
|
+
workspacePath: uniqueWorkdir,
|
|
860
|
+
agentName: this.agentName,
|
|
861
|
+
agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
|
|
862
|
+
});
|
|
863
|
+
validatePushAccess(uniqueWorkdir);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
// No repo info — just create the directory
|
|
867
|
+
fs.mkdirSync(uniqueWorkdir, { recursive: true });
|
|
868
|
+
this.agentConfig.workdir = uniqueWorkdir;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
// Directory exists with .git — pull latest
|
|
873
|
+
console.log(`Workspace exists, pulling latest...`);
|
|
874
|
+
try {
|
|
875
|
+
execSync("git fetch origin && git pull", {
|
|
876
|
+
cwd: uniqueWorkdir,
|
|
877
|
+
stdio: "pipe",
|
|
878
|
+
timeout: 30_000,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
console.warn(`Could not pull latest: ${error.message}`);
|
|
883
|
+
}
|
|
884
|
+
this.agentConfig.workdir = uniqueWorkdir;
|
|
885
|
+
}
|
|
959
886
|
return;
|
|
960
887
|
}
|
|
961
888
|
// No project.workdir set, check if we have a repo assignment
|
|
@@ -966,39 +893,29 @@ Logs: docker logs ${containerName}
|
|
|
966
893
|
const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${repoAssignment.project.code.toLowerCase()}/${this.agentName}`;
|
|
967
894
|
// If --auto-setup is enabled, automatically clone the repo
|
|
968
895
|
if (this.autoSetup) {
|
|
969
|
-
this.agentConfig.workdir =
|
|
896
|
+
this.agentConfig.workdir = setupWorkspace({
|
|
897
|
+
workspacePath: expandedPath,
|
|
898
|
+
repoUrl: repo.url,
|
|
899
|
+
defaultBranch: repo.default_branch,
|
|
900
|
+
projectName: repoAssignment.project.name,
|
|
901
|
+
});
|
|
902
|
+
// Configure git identity and validate push access
|
|
903
|
+
configureGitIdentity({
|
|
904
|
+
workspacePath: expandedPath,
|
|
905
|
+
agentName: this.agentName,
|
|
906
|
+
agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
|
|
907
|
+
});
|
|
908
|
+
validatePushAccess(expandedPath);
|
|
970
909
|
return;
|
|
971
910
|
}
|
|
972
|
-
console.error(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
`);
|
|
911
|
+
console.error(renderMissingWorkdirMessage({
|
|
912
|
+
projectName: repoAssignment.project.name,
|
|
913
|
+
repoFullName: repo.full_name,
|
|
914
|
+
repoUrl: repo.url,
|
|
915
|
+
defaultBranch: repo.default_branch,
|
|
916
|
+
suggestedPath,
|
|
917
|
+
agentName: this.agentName,
|
|
918
|
+
}));
|
|
1002
919
|
// No session to clean up - we haven't created it yet
|
|
1003
920
|
process.exit(1);
|
|
1004
921
|
}
|
|
@@ -1010,78 +927,61 @@ Option 3: Use --auto-setup to automatically clone the repository:
|
|
|
1010
927
|
}
|
|
1011
928
|
}
|
|
1012
929
|
/**
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
930
|
+
* Ensures the sandbox OpenCode config exists
|
|
931
|
+
* Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
|
|
1015
932
|
*/
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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`);
|
|
933
|
+
ensureSandboxOpencodeConfig() {
|
|
934
|
+
writeSandboxOpencodeConfig({
|
|
935
|
+
configPath: SANDBOX_OPENCODE_CONFIG_PATH,
|
|
936
|
+
baseConfig: SANDBOX_OPENCODE_CONFIG,
|
|
937
|
+
model: this.runnerConfig.env?.OPENCODE_MODEL,
|
|
938
|
+
});
|
|
939
|
+
console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Caches onboard data to disk for offline fallback
|
|
943
|
+
*/
|
|
944
|
+
cacheOnboardData() {
|
|
945
|
+
if (!this.onboardData || !this.agentId)
|
|
946
|
+
return;
|
|
1049
947
|
try {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
948
|
+
const cacheDir = path.join(os.homedir(), ".agentmesh", "context");
|
|
949
|
+
if (!fs.existsSync(cacheDir)) {
|
|
950
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
951
|
+
}
|
|
952
|
+
// Strip credential values before caching
|
|
953
|
+
const cacheData = {
|
|
954
|
+
...this.onboardData,
|
|
955
|
+
repos: this.onboardData.repos.map((r) => ({
|
|
956
|
+
...r,
|
|
957
|
+
credential: r.credential ? { type: r.credential.type, value: "***" } : null,
|
|
958
|
+
})),
|
|
959
|
+
cached_at: new Date().toISOString(),
|
|
960
|
+
};
|
|
961
|
+
const cachePath = path.join(cacheDir, `${this.agentId}-onboard.json`);
|
|
962
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
1054
963
|
}
|
|
1055
964
|
catch (error) {
|
|
1056
|
-
console.
|
|
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);
|
|
965
|
+
console.log("Failed to cache onboard data:", error.message);
|
|
1060
966
|
}
|
|
1061
|
-
console.log(`✓ Workspace ready: ${workspacePath}\n`);
|
|
1062
|
-
return workspacePath;
|
|
1063
967
|
}
|
|
1064
968
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
* Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
|
|
969
|
+
* Loads cached onboard data from disk (without credentials)
|
|
1067
970
|
*/
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
971
|
+
loadCachedOnboardData() {
|
|
972
|
+
if (!this.agentId)
|
|
973
|
+
return null;
|
|
974
|
+
try {
|
|
975
|
+
const cachePath = path.join(os.homedir(), ".agentmesh", "context", `${this.agentId}-onboard.json`);
|
|
976
|
+
if (!fs.existsSync(cachePath))
|
|
977
|
+
return null;
|
|
978
|
+
const raw = fs.readFileSync(cachePath, "utf-8");
|
|
979
|
+
const data = JSON.parse(raw);
|
|
980
|
+
return data;
|
|
1072
981
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
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;
|
|
982
|
+
catch {
|
|
983
|
+
return null;
|
|
1081
984
|
}
|
|
1082
|
-
// Always write to ensure model is up to date
|
|
1083
|
-
fs.writeFileSync(SANDBOX_OPENCODE_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
1084
|
-
console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
|
|
1085
985
|
}
|
|
1086
986
|
}
|
|
1087
987
|
//# sourceMappingURL=daemon.js.map
|