@agentmeshhq/agent 0.2.1 → 0.4.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 +6 -0
- package/dist/__tests__/context-template.test.d.ts +4 -0
- package/dist/__tests__/context-template.test.js +235 -0
- package/dist/__tests__/context-template.test.js.map +1 -0
- package/dist/__tests__/default-branch-fallback.test.d.ts +10 -0
- package/dist/__tests__/default-branch-fallback.test.js +123 -0
- package/dist/__tests__/default-branch-fallback.test.js.map +1 -0
- package/dist/__tests__/default-repo-fallback.test.d.ts +12 -0
- package/dist/__tests__/default-repo-fallback.test.js +156 -0
- package/dist/__tests__/default-repo-fallback.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 +343 -0
- package/dist/__tests__/onboard.test.js.map +1 -0
- package/dist/__tests__/project-flag.test.d.ts +9 -0
- package/dist/__tests__/project-flag.test.js +140 -0
- package/dist/__tests__/project-flag.test.js.map +1 -0
- 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/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +4 -0
- package/dist/cli/start.js +6 -0
- package/dist/cli/start.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 +2 -2
- package/dist/core/daemon/bootstrap.d.ts +4 -0
- package/dist/core/daemon/bootstrap.js +2 -0
- package/dist/core/daemon/bootstrap.js.map +1 -1
- 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 +0 -2
- package/dist/core/daemon/crash-log.js +0 -1
- package/dist/core/daemon/crash-log.js.map +1 -1
- package/dist/core/daemon/git-auth.d.ts +18 -0
- package/dist/core/daemon/git-auth.js +94 -0
- package/dist/core/daemon/git-auth.js.map +1 -0
- package/dist/core/daemon/health-policy.d.ts +0 -4
- package/dist/core/daemon/health-policy.js +0 -8
- package/dist/core/daemon/health-policy.js.map +1 -1
- package/dist/core/daemon/state.js +1 -0
- package/dist/core/daemon/state.js.map +1 -1
- package/dist/core/daemon/workspace.d.ts +13 -0
- package/dist/core/daemon/workspace.js +39 -0
- package/dist/core/daemon/workspace.js.map +1 -1
- package/dist/core/daemon.d.ts +19 -6
- package/dist/core/daemon.js +337 -199
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.d.ts +5 -1
- package/dist/core/injector.js +77 -0
- package/dist/core/injector.js.map +1 -1
- package/dist/core/registry.d.ts +97 -0
- package/dist/core/registry.js +59 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/tmux-runtime.js +4 -0
- package/dist/core/tmux-runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/core/daemon.js
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
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 { getAgentState,
|
|
5
|
+
import { getAgentState, loadState, updateAgentInState } from "../config/loader.js";
|
|
6
6
|
import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
|
|
7
7
|
import { renderMissingWorkdirMessage } from "./daemon/assignment-message.js";
|
|
8
8
|
import { bootstrapDaemon } from "./daemon/bootstrap.js";
|
|
9
|
+
import { removeClaudeMd, writeClaudeMd } from "./daemon/context-template.js";
|
|
9
10
|
import { formatCrashLog } from "./daemon/crash-log.js";
|
|
10
|
-
import {
|
|
11
|
+
import { cleanupGitAuth, setupGitAuth } from "./daemon/git-auth.js";
|
|
12
|
+
import { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, } from "./daemon/health-policy.js";
|
|
11
13
|
import { writeSandboxOpencodeConfig } from "./daemon/sandbox-config.js";
|
|
12
14
|
import { captureAgentChildPids, persistRunningState } from "./daemon/state.js";
|
|
13
15
|
import { startTmuxRuntimeSession } from "./daemon/tmux-session.js";
|
|
14
|
-
import { setupWorkspace } from "./daemon/workspace.js";
|
|
16
|
+
import { configureGitIdentity, setupWorkspace, validatePushAccess } from "./daemon/workspace.js";
|
|
15
17
|
import { Heartbeat } from "./heartbeat.js";
|
|
16
|
-
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
17
|
-
import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
|
|
18
|
+
import { handleWebSocketEvent, injectOnboardMessage, injectRestoredContext, injectStartupMessage, } from "./injector.js";
|
|
19
|
+
import { checkInbox, createSelfAssignment, fetchAssignments, fetchOnboard, fetchProjectByCode, registerAgent, } from "./registry.js";
|
|
18
20
|
import { getRunnerDisplayName } from "./runner.js";
|
|
19
21
|
import { DockerSandbox } from "./sandbox.js";
|
|
20
|
-
import { getLatestSessionId,
|
|
21
|
-
import { captureSessionContext, captureSessionOutput,
|
|
22
|
+
import { getLatestSessionId, waitForNewSessionId } from "./session-id.js";
|
|
23
|
+
import { captureSessionContext, captureSessionOutput, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
|
|
22
24
|
import { prepareOpenCodeRuntime } from "./tmux-runtime.js";
|
|
23
25
|
import { checkAgentProgress, cleanupOrphanContainers, isProcessRunning, sendNudge, } from "./watchdog.js";
|
|
24
26
|
import { AgentWebSocket } from "./websocket.js";
|
|
25
|
-
//
|
|
26
|
-
const MAX_RESTART_ATTEMPTS = 3;
|
|
27
|
-
// Time after which restart count resets (30 minutes of stable operation)
|
|
28
|
-
const RESTART_COUNT_RESET_MS = 30 * 60 * 1000;
|
|
29
|
-
// Time to wait after nudging before restarting (2 minutes)
|
|
27
|
+
// Time to wait after nudging before marking as stuck (2 minutes)
|
|
30
28
|
const NUDGE_WAIT_MS = 2 * 60 * 1000;
|
|
31
29
|
// Path to the sandbox OpenCode config (permissive permissions)
|
|
32
30
|
const SANDBOX_OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".agentmesh", "opencode-sandbox.json");
|
|
@@ -56,14 +54,15 @@ export class AgentDaemon {
|
|
|
56
54
|
sandboxImage;
|
|
57
55
|
sandboxCpu;
|
|
58
56
|
sandboxMemory;
|
|
57
|
+
onboardData = null;
|
|
59
58
|
sandbox = null;
|
|
59
|
+
projectCode;
|
|
60
|
+
projectRole;
|
|
60
61
|
healthCheckInterval = null;
|
|
61
62
|
// Session resume tracking
|
|
62
63
|
_preStartSessionId;
|
|
63
64
|
_attemptedResumeSessionId;
|
|
64
|
-
//
|
|
65
|
-
restartCount = 0;
|
|
66
|
-
lastStableTime = null;
|
|
65
|
+
// Stuck detection tracking
|
|
67
66
|
stuckSince = null;
|
|
68
67
|
nudgeSentAt = null;
|
|
69
68
|
constructor(options) {
|
|
@@ -80,6 +79,8 @@ export class AgentDaemon {
|
|
|
80
79
|
this.sandboxImage = boot.sandboxImage;
|
|
81
80
|
this.sandboxCpu = boot.sandboxCpu;
|
|
82
81
|
this.sandboxMemory = boot.sandboxMemory;
|
|
82
|
+
this.projectCode = boot.projectCode;
|
|
83
|
+
this.projectRole = boot.projectRole;
|
|
83
84
|
this.runnerConfig = boot.runnerConfig;
|
|
84
85
|
const runnerName = getRunnerDisplayName(this.runnerConfig.type);
|
|
85
86
|
console.log(`Runner: ${runnerName}`);
|
|
@@ -113,9 +114,6 @@ export class AgentDaemon {
|
|
|
113
114
|
console.log(`Cleaned up ${cleaned} orphan container(s)`);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
// Reset restart count on manual start
|
|
117
|
-
this.restartCount = 0;
|
|
118
|
-
this.lastStableTime = new Date();
|
|
119
117
|
// Register with hub first (needed for assignment check)
|
|
120
118
|
console.log("Registering with AgentMesh hub...");
|
|
121
119
|
console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
|
|
@@ -139,8 +137,70 @@ export class AgentDaemon {
|
|
|
139
137
|
else {
|
|
140
138
|
console.log(`Registered as: ${this.agentId}`);
|
|
141
139
|
}
|
|
142
|
-
//
|
|
143
|
-
|
|
140
|
+
// Fetch onboard payload (project context, credentials, team)
|
|
141
|
+
try {
|
|
142
|
+
this.onboardData = await fetchOnboard(this.config.hubUrl, this.token);
|
|
143
|
+
if (this.onboardData) {
|
|
144
|
+
console.log(`Onboard data received${this.onboardData.project ? `: project=${this.onboardData.project.name}` : ""}`);
|
|
145
|
+
// Set up git credentials before workspace setup
|
|
146
|
+
for (const repo of this.onboardData.repos) {
|
|
147
|
+
if (repo.credential) {
|
|
148
|
+
const extraEnv = setupGitAuth({
|
|
149
|
+
type: repo.credential.type,
|
|
150
|
+
value: repo.credential.value,
|
|
151
|
+
repoUrl: repo.url,
|
|
152
|
+
agentName: this.agentName,
|
|
153
|
+
});
|
|
154
|
+
Object.assign(this.runnerConfig.env, extraEnv);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Cache onboard data locally for offline fallback
|
|
158
|
+
this.cacheOnboardData();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.log("Onboard fetch failed (non-fatal):", error.message);
|
|
163
|
+
// Try loading cached onboard data
|
|
164
|
+
this.onboardData = this.loadCachedOnboardData();
|
|
165
|
+
if (this.onboardData) {
|
|
166
|
+
console.log("Loaded cached onboard data");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Resolve workdir: --project flag takes precedence, otherwise fall back to assignments
|
|
170
|
+
if (this.projectCode) {
|
|
171
|
+
await this.resolveProjectWorkdir();
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await this.checkAssignments();
|
|
175
|
+
}
|
|
176
|
+
// Enforce unique working directories — no two running agents may share the same workdir
|
|
177
|
+
if (this.agentConfig.workdir) {
|
|
178
|
+
const resolvedWorkdir = path.resolve(this.agentConfig.workdir);
|
|
179
|
+
const state = loadState();
|
|
180
|
+
const conflict = state.agents.find((a) => a.name !== this.agentName &&
|
|
181
|
+
a.workdir &&
|
|
182
|
+
path.resolve(a.workdir) === resolvedWorkdir &&
|
|
183
|
+
a.pid > 0 &&
|
|
184
|
+
isProcessRunning(a.pid));
|
|
185
|
+
if (conflict) {
|
|
186
|
+
throw new Error(`Workdir conflict: "${resolvedWorkdir}" is already in use by agent "${conflict.name}" (PID: ${conflict.pid}).\n` +
|
|
187
|
+
`Each agent must have its own working directory to avoid git conflicts.\n` +
|
|
188
|
+
`Use --workdir to specify a unique path, e.g.:\n` +
|
|
189
|
+
` agentmesh start --name ${this.agentName} --workdir ~/Dev/${this.assignedProject || "project"}-${this.agentName}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Check for serve port collision
|
|
193
|
+
if (this.serveMode) {
|
|
194
|
+
const state = loadState();
|
|
195
|
+
const portConflict = state.agents.find((a) => a.name !== this.agentName &&
|
|
196
|
+
a.servePort === this.servePort &&
|
|
197
|
+
a.pid > 0 &&
|
|
198
|
+
isProcessRunning(a.pid));
|
|
199
|
+
if (portConflict) {
|
|
200
|
+
throw new Error(`Port ${this.servePort} already in use by agent "${portConflict.name}" (PID: ${portConflict.pid}).\n` +
|
|
201
|
+
`Use --serve-port to specify a different port.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
144
204
|
// Choose runtime mode: sandbox > serve > tmux
|
|
145
205
|
if (this.sandboxMode) {
|
|
146
206
|
await this.startSandboxMode();
|
|
@@ -277,6 +337,20 @@ export class AgentDaemon {
|
|
|
277
337
|
console.error("Failed to check inbox:", error);
|
|
278
338
|
injectStartupMessage(this.agentName, 0);
|
|
279
339
|
}
|
|
340
|
+
// Inject onboard project context
|
|
341
|
+
if (this.onboardData?.project) {
|
|
342
|
+
injectOnboardMessage(this.agentName, this.onboardData);
|
|
343
|
+
}
|
|
344
|
+
// Write persistent CLAUDE.md context file
|
|
345
|
+
if (this.onboardData && this.agentConfig.workdir) {
|
|
346
|
+
const mdPath = writeClaudeMd({
|
|
347
|
+
workdir: this.agentConfig.workdir,
|
|
348
|
+
onboard: this.onboardData,
|
|
349
|
+
});
|
|
350
|
+
if (mdPath) {
|
|
351
|
+
console.log(`✓ CLAUDE.md written: ${mdPath}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
280
354
|
// Restore context from previous session
|
|
281
355
|
if (this.shouldRestoreContext && this.agentId) {
|
|
282
356
|
console.log("Checking for previous context...");
|
|
@@ -368,17 +442,11 @@ Nudge agent:
|
|
|
368
442
|
this.healthCheckInterval = setInterval(async () => {
|
|
369
443
|
if (!this.isRunning)
|
|
370
444
|
return;
|
|
371
|
-
// Reset restart count after stable operation
|
|
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);
|
|
376
|
-
}
|
|
377
445
|
// For sandbox mode, pass container name so health check looks inside container
|
|
378
446
|
const containerName = this.sandboxMode ? this.sandbox?.getContainerName() : undefined;
|
|
379
447
|
const health = isSessionHealthy(this.agentName, containerName);
|
|
380
448
|
if (!health.healthy) {
|
|
381
|
-
// Session died -
|
|
449
|
+
// Session died - log and mark as failed (no auto-restart)
|
|
382
450
|
await this.handleSessionDeath(health.reason || "unknown", logDir);
|
|
383
451
|
return;
|
|
384
452
|
}
|
|
@@ -395,7 +463,7 @@ Nudge agent:
|
|
|
395
463
|
console.log(`[HEALTH] Agent is waiting for human input: ${progress.details}`);
|
|
396
464
|
}
|
|
397
465
|
else if (progress.status === "permission_blocked" || progress.status === "stuck") {
|
|
398
|
-
|
|
466
|
+
this.handleStuckAgent(progress);
|
|
399
467
|
}
|
|
400
468
|
else if (progress.status === "active") {
|
|
401
469
|
// Agent is working - reset stuck tracking
|
|
@@ -405,7 +473,6 @@ Nudge agent:
|
|
|
405
473
|
this.nudgeSentAt = null;
|
|
406
474
|
updateAgentInState(this.agentName, { stuckSince: undefined, status: "running" });
|
|
407
475
|
}
|
|
408
|
-
this.lastStableTime = new Date();
|
|
409
476
|
}
|
|
410
477
|
}, 60000); // Check every 60 seconds
|
|
411
478
|
}
|
|
@@ -428,57 +495,34 @@ Nudge agent:
|
|
|
428
495
|
agentName: this.agentName,
|
|
429
496
|
agentId: this.agentId,
|
|
430
497
|
reason,
|
|
431
|
-
restartCount: this.restartCount,
|
|
432
|
-
maxRestartAttempts: MAX_RESTART_ATTEMPTS,
|
|
433
498
|
sandboxLabel: this.sandboxMode ? this.sandbox?.getContainerName() || "sandbox" : "none",
|
|
434
499
|
workdir: this.agentConfig.workdir,
|
|
435
500
|
model: this.runnerConfig.model,
|
|
436
501
|
lastOutput,
|
|
437
502
|
});
|
|
438
503
|
fs.appendFileSync(logFile, crashLog);
|
|
439
|
-
// Save context
|
|
504
|
+
// Save context before marking as failed
|
|
440
505
|
if (this.agentId) {
|
|
441
506
|
this.saveAgentContext();
|
|
442
507
|
}
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
this.lastStableTime = new Date();
|
|
456
|
-
}
|
|
457
|
-
catch (error) {
|
|
458
|
-
console.error(`[RESTART] Failed to restart: ${error.message}`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// Exceeded restart limit - mark as failed
|
|
463
|
-
console.error(`[FAILED] Agent exceeded restart limit (${MAX_RESTART_ATTEMPTS}). Manual intervention required.`);
|
|
464
|
-
// Terminal bell to alert user
|
|
465
|
-
process.stdout.write("\x07");
|
|
466
|
-
updateAgentInState(this.agentName, {
|
|
467
|
-
status: "failed",
|
|
468
|
-
restartCount: this.restartCount,
|
|
469
|
-
});
|
|
470
|
-
// Stop monitoring
|
|
471
|
-
this.isRunning = false;
|
|
472
|
-
if (this.healthCheckInterval) {
|
|
473
|
-
clearInterval(this.healthCheckInterval);
|
|
474
|
-
this.healthCheckInterval = null;
|
|
475
|
-
}
|
|
508
|
+
// Mark as failed — no auto-restart, user must intervene
|
|
509
|
+
console.error(`[FAILED] Session died: ${reason}. Manual intervention required (use 'agentmesh restart ${this.agentName}').`);
|
|
510
|
+
// Terminal bell to alert user
|
|
511
|
+
process.stdout.write("\x07");
|
|
512
|
+
updateAgentInState(this.agentName, {
|
|
513
|
+
status: "failed",
|
|
514
|
+
});
|
|
515
|
+
// Stop monitoring
|
|
516
|
+
this.isRunning = false;
|
|
517
|
+
if (this.healthCheckInterval) {
|
|
518
|
+
clearInterval(this.healthCheckInterval);
|
|
519
|
+
this.healthCheckInterval = null;
|
|
476
520
|
}
|
|
477
521
|
}
|
|
478
522
|
/**
|
|
479
523
|
* Handles stuck agent - sends nudge first, then restarts if still stuck
|
|
480
524
|
*/
|
|
481
|
-
|
|
525
|
+
handleStuckAgent(progress) {
|
|
482
526
|
const now = new Date();
|
|
483
527
|
if (!this.stuckSince) {
|
|
484
528
|
// First detection of stuck state
|
|
@@ -489,7 +533,7 @@ Nudge agent:
|
|
|
489
533
|
status: "stuck",
|
|
490
534
|
});
|
|
491
535
|
}
|
|
492
|
-
//
|
|
536
|
+
// Nudge worker agents — don't escalate to restart
|
|
493
537
|
if (this.isWorkerAgent) {
|
|
494
538
|
// If we haven't sent a nudge yet, send one
|
|
495
539
|
if (!this.nudgeSentAt) {
|
|
@@ -510,124 +554,9 @@ Nudge agent:
|
|
|
510
554
|
// Still waiting for agent to respond to nudge
|
|
511
555
|
return;
|
|
512
556
|
}
|
|
557
|
+
// Nudge grace period expired — log warning but do NOT restart
|
|
558
|
+
console.log(`[HEALTH] Agent still stuck after nudge. Manual intervention required.`);
|
|
513
559
|
}
|
|
514
|
-
// Agent still stuck - trigger restart (or restart immediately if not a worker)
|
|
515
|
-
console.log(`[HEALTH] Agent still stuck${this.isWorkerAgent ? " after nudge" : ""}, triggering restart...`);
|
|
516
|
-
this.stuckSince = null;
|
|
517
|
-
this.nudgeSentAt = null;
|
|
518
|
-
await this.handleSessionDeath("stuck_after_nudge", path.join(os.homedir(), ".agentmesh", "logs"));
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Restarts the agent session (sandbox or non-sandbox)
|
|
522
|
-
*/
|
|
523
|
-
async restartSession() {
|
|
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));
|
|
531
|
-
if (this.sandboxMode && this.sandbox) {
|
|
532
|
-
// Restart sandbox container
|
|
533
|
-
const newContainerId = await this.sandbox.restart();
|
|
534
|
-
console.log(`[RESTART] New container: ${newContainerId.substring(0, 12)}`);
|
|
535
|
-
// Recreate tmux session for sandbox
|
|
536
|
-
const containerName = this.sandbox.getContainerName();
|
|
537
|
-
// Build environment args for docker exec
|
|
538
|
-
const envArgs = [];
|
|
539
|
-
const allEnv = {
|
|
540
|
-
...this.runnerConfig.env,
|
|
541
|
-
AGENT_TOKEN: this.token,
|
|
542
|
-
AGENTMESH_AGENT_ID: this.agentId,
|
|
543
|
-
};
|
|
544
|
-
for (const [key, value] of Object.entries(allEnv)) {
|
|
545
|
-
if (value !== undefined && value !== "") {
|
|
546
|
-
envArgs.push(`-e "${key}=${value}"`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const envString = envArgs.join(" ");
|
|
550
|
-
const modelArg = this.runnerConfig.env?.OPENCODE_MODEL
|
|
551
|
-
? ` --model ${this.runnerConfig.env.OPENCODE_MODEL}`
|
|
552
|
-
: "";
|
|
553
|
-
const dockerExecCommand = `docker exec -it ${envString} ${containerName} opencode${modelArg}`;
|
|
554
|
-
const created = createSession(this.agentName, dockerExecCommand, undefined, undefined);
|
|
555
|
-
if (!created) {
|
|
556
|
-
throw new Error("Failed to create tmux session for restarted sandbox");
|
|
557
|
-
}
|
|
558
|
-
// Track new child PIDs and update state
|
|
559
|
-
const newChildPids = captureAgentChildPids(this.agentName);
|
|
560
|
-
updateAgentInState(this.agentName, {
|
|
561
|
-
sandboxContainer: containerName,
|
|
562
|
-
childPids: newChildPids,
|
|
563
|
-
});
|
|
564
|
-
if (newChildPids.length > 0) {
|
|
565
|
-
console.log(`[RESTART] Tracking ${newChildPids.length} child PIDs: ${newChildPids.join(", ")}`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
// Non-sandbox restart — load saved session ID for native resume
|
|
570
|
-
let savedSessionId;
|
|
571
|
-
let savedContext = null;
|
|
572
|
-
if (this.agentId) {
|
|
573
|
-
savedContext = loadContext(this.agentId);
|
|
574
|
-
savedSessionId = savedContext?.custom?.opencodeSessionId;
|
|
575
|
-
if (savedSessionId) {
|
|
576
|
-
console.log(`[RESTART] Attempting to resume OpenCode session: ${savedSessionId}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const preRestartSessionId = snapshotSessionId(this.agentName);
|
|
580
|
-
const created = createSession(this.agentName, this.agentConfig.command, this.agentConfig.workdir, this.runnerConfig.env, savedSessionId);
|
|
581
|
-
if (!created) {
|
|
582
|
-
throw new Error("Failed to create tmux session");
|
|
583
|
-
}
|
|
584
|
-
// Re-inject environment
|
|
585
|
-
updateSessionEnvironment(this.agentName, {
|
|
586
|
-
AGENT_TOKEN: this.token,
|
|
587
|
-
AGENTMESH_AGENT_ID: this.agentId,
|
|
588
|
-
...this.runnerConfig.env,
|
|
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
|
-
}
|
|
596
|
-
// Verify native resume and fallback if needed
|
|
597
|
-
if (savedSessionId && savedContext) {
|
|
598
|
-
const newSessionId = await waitForNewSessionId(this.agentName, preRestartSessionId, 15000);
|
|
599
|
-
if (!newSessionId) {
|
|
600
|
-
const health = isSessionHealthy(this.agentName);
|
|
601
|
-
const currentSessionId = getLatestSessionId(this.agentName);
|
|
602
|
-
if (!health.healthy) {
|
|
603
|
-
console.log(`[RESTART] Fallback: OpenCode not healthy, injecting text summary.`);
|
|
604
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
605
|
-
saveContext(savedContext);
|
|
606
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
607
|
-
}
|
|
608
|
-
else if (currentSessionId === savedSessionId) {
|
|
609
|
-
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
console.log(`[RESTART] Fallback: session not found. Injecting text summary.`);
|
|
613
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
614
|
-
saveContext(savedContext);
|
|
615
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
else if (newSessionId === savedSessionId) {
|
|
619
|
-
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
console.log(`[RESTART] Fallback: resume failed (got ${newSessionId}). Injecting text summary.`);
|
|
623
|
-
savedContext.custom = { ...savedContext.custom, opencodeSessionId: newSessionId };
|
|
624
|
-
saveContext(savedContext);
|
|
625
|
-
injectRestoredContext(this.agentName, savedContext);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
// Wait for session to be ready
|
|
630
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
631
560
|
}
|
|
632
561
|
async stop() {
|
|
633
562
|
console.log(`\nStopping agent: ${this.agentName}`);
|
|
@@ -642,6 +571,12 @@ Nudge agent:
|
|
|
642
571
|
console.log("Saving agent context...");
|
|
643
572
|
this.saveAgentContext();
|
|
644
573
|
}
|
|
574
|
+
// Clean up git credential files
|
|
575
|
+
cleanupGitAuth(this.agentName);
|
|
576
|
+
// Remove CLAUDE.md context file
|
|
577
|
+
if (this.agentConfig.workdir) {
|
|
578
|
+
removeClaudeMd(this.agentConfig.workdir);
|
|
579
|
+
}
|
|
645
580
|
// Stop heartbeat
|
|
646
581
|
if (this.heartbeat) {
|
|
647
582
|
this.heartbeat.stop();
|
|
@@ -887,6 +822,84 @@ Logs: docker logs ${containerName}
|
|
|
887
822
|
console.error("Failed to save agent context:", error);
|
|
888
823
|
}
|
|
889
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Resolves workdir from --project flag: looks up project by code, clones repo, self-assigns.
|
|
827
|
+
*/
|
|
828
|
+
async resolveProjectWorkdir() {
|
|
829
|
+
if (!this.token || !this.agentId || !this.projectCode)
|
|
830
|
+
return;
|
|
831
|
+
console.log(`Resolving project "${this.projectCode}" from HQ...`);
|
|
832
|
+
const result = await fetchProjectByCode(this.config.hubUrl, this.token, this.projectCode);
|
|
833
|
+
if (!result) {
|
|
834
|
+
throw new Error(`Project "${this.projectCode}" not found. Check the project code in the admin UI.`);
|
|
835
|
+
}
|
|
836
|
+
const { project, repo } = result;
|
|
837
|
+
this.assignedProject = project.name;
|
|
838
|
+
console.log(`Project found: ${project.name} (${project.code})`);
|
|
839
|
+
if (!repo) {
|
|
840
|
+
throw new Error(`Project "${project.name}" has no repositories configured. Add a repo in the admin UI first.`);
|
|
841
|
+
}
|
|
842
|
+
// Compute per-agent workdir
|
|
843
|
+
let uniqueWorkdir;
|
|
844
|
+
if (project.workdir) {
|
|
845
|
+
// Sibling directory: /Users/raju/Dev/agentmesh → /Users/raju/Dev/agentmesh-agent-1
|
|
846
|
+
uniqueWorkdir = path.join(path.dirname(project.workdir), `${path.basename(project.workdir)}-${this.agentName}`);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
// Fallback to managed workspace path
|
|
850
|
+
uniqueWorkdir = path.join(os.homedir(), ".agentmesh", "workspaces", this.config.workspace, project.code.toLowerCase(), this.agentName);
|
|
851
|
+
}
|
|
852
|
+
console.log(`Per-agent workdir: ${uniqueWorkdir}`);
|
|
853
|
+
// Clone or pull
|
|
854
|
+
const gitDir = path.join(uniqueWorkdir, ".git");
|
|
855
|
+
if (!fs.existsSync(gitDir)) {
|
|
856
|
+
this.agentConfig.workdir = setupWorkspace({
|
|
857
|
+
workspacePath: uniqueWorkdir,
|
|
858
|
+
repoUrl: repo.url,
|
|
859
|
+
defaultBranch: repo.default_branch ?? project.default_branch ?? "main",
|
|
860
|
+
projectName: project.name,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
console.log(`Workspace exists, pulling latest...`);
|
|
865
|
+
try {
|
|
866
|
+
execSync("git fetch origin && git pull", {
|
|
867
|
+
cwd: uniqueWorkdir,
|
|
868
|
+
stdio: "pipe",
|
|
869
|
+
timeout: 30_000,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
console.warn(`Could not pull latest: ${error.message}`);
|
|
874
|
+
}
|
|
875
|
+
this.agentConfig.workdir = uniqueWorkdir;
|
|
876
|
+
}
|
|
877
|
+
// Configure git identity + validate push access
|
|
878
|
+
configureGitIdentity({
|
|
879
|
+
workspacePath: uniqueWorkdir,
|
|
880
|
+
agentName: this.agentName,
|
|
881
|
+
agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
|
|
882
|
+
});
|
|
883
|
+
validatePushAccess(uniqueWorkdir);
|
|
884
|
+
// Auto-assign the agent to the project
|
|
885
|
+
try {
|
|
886
|
+
const assignmentResult = await createSelfAssignment(this.config.hubUrl, this.token, {
|
|
887
|
+
workspace_id: this.config.workspace,
|
|
888
|
+
project_id: project.project_id,
|
|
889
|
+
agent_id: this.agentId,
|
|
890
|
+
role: this.projectRole,
|
|
891
|
+
});
|
|
892
|
+
if (assignmentResult === "existing") {
|
|
893
|
+
console.log(`Already assigned to project ${project.name}`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
console.log(`Auto-assigned to project ${project.name} as ${this.projectRole}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
console.warn(`Could not auto-assign to project: ${error.message}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
890
903
|
/**
|
|
891
904
|
* Fetches assignments from HQ and validates workdir setup
|
|
892
905
|
* Uses project.workdir from HQ as source of truth, falls back to helpful instructions
|
|
@@ -912,31 +925,111 @@ Logs: docker logs ${containerName}
|
|
|
912
925
|
if (!this.agentConfig.workdir) {
|
|
913
926
|
const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
|
|
914
927
|
if (assignmentWithWorkdir?.project.workdir) {
|
|
915
|
-
|
|
916
|
-
|
|
928
|
+
// Append agent name as sibling directory to avoid shared workdir conflicts
|
|
929
|
+
// e.g. /Users/raju/Dev/agentmesh -> /Users/raju/Dev/agentmesh-agent-1
|
|
930
|
+
const baseWorkdir = assignmentWithWorkdir.project.workdir;
|
|
931
|
+
const uniqueWorkdir = path.join(path.dirname(baseWorkdir), `${path.basename(baseWorkdir)}-${this.agentName}`);
|
|
932
|
+
console.log(`Using workdir from project settings (per-agent): ${uniqueWorkdir}`);
|
|
933
|
+
// If the unique dir doesn't exist, set up workspace (clone or pull)
|
|
934
|
+
const gitDir = path.join(uniqueWorkdir, ".git");
|
|
935
|
+
if (!fs.existsSync(gitDir)) {
|
|
936
|
+
// Find repo info from assignments or onboard data for cloning
|
|
937
|
+
const repoAssignment = assignments.find((a) => a.repo !== null);
|
|
938
|
+
const projectDefaultBranch = assignments[0]?.project.default_branch ?? null;
|
|
939
|
+
const projectDefaultRepoId = assignments[0]?.project.default_repo_id ?? null;
|
|
940
|
+
const defaultRepo = projectDefaultRepoId
|
|
941
|
+
? this.onboardData?.repos.find((r) => r.repo_id === projectDefaultRepoId)
|
|
942
|
+
: null;
|
|
943
|
+
const fallbackRepo = defaultRepo ?? this.onboardData?.repos[0];
|
|
944
|
+
const repoInfo = repoAssignment?.repo ??
|
|
945
|
+
(fallbackRepo
|
|
946
|
+
? {
|
|
947
|
+
url: fallbackRepo.url,
|
|
948
|
+
default_branch: projectDefaultBranch ?? fallbackRepo.default_branch ?? "main",
|
|
949
|
+
full_name: fallbackRepo.full_name,
|
|
950
|
+
}
|
|
951
|
+
: null);
|
|
952
|
+
const projectName = repoAssignment?.project.name ?? assignments[0]?.project.name ?? "unknown";
|
|
953
|
+
if (repoInfo) {
|
|
954
|
+
this.agentConfig.workdir = setupWorkspace({
|
|
955
|
+
workspacePath: uniqueWorkdir,
|
|
956
|
+
repoUrl: repoInfo.url,
|
|
957
|
+
defaultBranch: repoInfo.default_branch,
|
|
958
|
+
projectName,
|
|
959
|
+
});
|
|
960
|
+
configureGitIdentity({
|
|
961
|
+
workspacePath: uniqueWorkdir,
|
|
962
|
+
agentName: this.agentName,
|
|
963
|
+
agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
|
|
964
|
+
});
|
|
965
|
+
validatePushAccess(uniqueWorkdir);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
// Genuinely no repo info — just create the directory
|
|
969
|
+
fs.mkdirSync(uniqueWorkdir, { recursive: true });
|
|
970
|
+
this.agentConfig.workdir = uniqueWorkdir;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
// Directory exists with .git — pull latest
|
|
975
|
+
console.log(`Workspace exists, pulling latest...`);
|
|
976
|
+
try {
|
|
977
|
+
execSync("git fetch origin && git pull", {
|
|
978
|
+
cwd: uniqueWorkdir,
|
|
979
|
+
stdio: "pipe",
|
|
980
|
+
timeout: 30_000,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
console.warn(`Could not pull latest: ${error.message}`);
|
|
985
|
+
}
|
|
986
|
+
this.agentConfig.workdir = uniqueWorkdir;
|
|
987
|
+
}
|
|
917
988
|
return;
|
|
918
989
|
}
|
|
919
|
-
// No project.workdir set, check if we have a repo assignment
|
|
990
|
+
// No project.workdir set, check if we have a repo assignment or onboard repo
|
|
920
991
|
const repoAssignment = assignments.find((a) => a.repo !== null);
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
992
|
+
const projectDefaultBranch = assignments[0]?.project.default_branch ?? null;
|
|
993
|
+
const projectDefaultRepoId = assignments[0]?.project.default_repo_id ?? null;
|
|
994
|
+
const defaultRepo = projectDefaultRepoId
|
|
995
|
+
? this.onboardData?.repos.find((r) => r.repo_id === projectDefaultRepoId)
|
|
996
|
+
: null;
|
|
997
|
+
const fallbackRepo = defaultRepo ?? this.onboardData?.repos[0];
|
|
998
|
+
const repoInfo = repoAssignment?.repo ??
|
|
999
|
+
(fallbackRepo
|
|
1000
|
+
? {
|
|
1001
|
+
url: fallbackRepo.url,
|
|
1002
|
+
default_branch: projectDefaultBranch ?? fallbackRepo.default_branch ?? "main",
|
|
1003
|
+
full_name: fallbackRepo.full_name,
|
|
1004
|
+
}
|
|
1005
|
+
: null);
|
|
1006
|
+
const projectCode = repoAssignment?.project.code ?? assignments[0]?.project.code ?? "unknown";
|
|
1007
|
+
const projectName = repoAssignment?.project.name ?? assignments[0]?.project.name ?? "unknown";
|
|
1008
|
+
if (repoInfo) {
|
|
1009
|
+
const expandedPath = path.join(os.homedir(), ".agentmesh", "workspaces", this.config.workspace, projectCode.toLowerCase(), this.agentName);
|
|
1010
|
+
const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${projectCode.toLowerCase()}/${this.agentName}`;
|
|
925
1011
|
// If --auto-setup is enabled, automatically clone the repo
|
|
926
1012
|
if (this.autoSetup) {
|
|
927
1013
|
this.agentConfig.workdir = setupWorkspace({
|
|
928
1014
|
workspacePath: expandedPath,
|
|
929
|
-
repoUrl:
|
|
930
|
-
defaultBranch:
|
|
931
|
-
projectName
|
|
1015
|
+
repoUrl: repoInfo.url,
|
|
1016
|
+
defaultBranch: repoInfo.default_branch,
|
|
1017
|
+
projectName,
|
|
1018
|
+
});
|
|
1019
|
+
// Configure git identity and validate push access
|
|
1020
|
+
configureGitIdentity({
|
|
1021
|
+
workspacePath: expandedPath,
|
|
1022
|
+
agentName: this.agentName,
|
|
1023
|
+
agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
|
|
932
1024
|
});
|
|
1025
|
+
validatePushAccess(expandedPath);
|
|
933
1026
|
return;
|
|
934
1027
|
}
|
|
935
1028
|
console.error(renderMissingWorkdirMessage({
|
|
936
|
-
projectName
|
|
937
|
-
repoFullName:
|
|
938
|
-
repoUrl:
|
|
939
|
-
defaultBranch:
|
|
1029
|
+
projectName,
|
|
1030
|
+
repoFullName: repoInfo.full_name,
|
|
1031
|
+
repoUrl: repoInfo.url,
|
|
1032
|
+
defaultBranch: repoInfo.default_branch,
|
|
940
1033
|
suggestedPath,
|
|
941
1034
|
agentName: this.agentName,
|
|
942
1035
|
}));
|
|
@@ -962,5 +1055,50 @@ Logs: docker logs ${containerName}
|
|
|
962
1055
|
});
|
|
963
1056
|
console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
|
|
964
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Caches onboard data to disk for offline fallback
|
|
1060
|
+
*/
|
|
1061
|
+
cacheOnboardData() {
|
|
1062
|
+
if (!this.onboardData || !this.agentId)
|
|
1063
|
+
return;
|
|
1064
|
+
try {
|
|
1065
|
+
const cacheDir = path.join(os.homedir(), ".agentmesh", "context");
|
|
1066
|
+
if (!fs.existsSync(cacheDir)) {
|
|
1067
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1068
|
+
}
|
|
1069
|
+
// Strip credential values before caching
|
|
1070
|
+
const cacheData = {
|
|
1071
|
+
...this.onboardData,
|
|
1072
|
+
repos: this.onboardData.repos.map((r) => ({
|
|
1073
|
+
...r,
|
|
1074
|
+
credential: r.credential ? { type: r.credential.type, value: "***" } : null,
|
|
1075
|
+
})),
|
|
1076
|
+
cached_at: new Date().toISOString(),
|
|
1077
|
+
};
|
|
1078
|
+
const cachePath = path.join(cacheDir, `${this.agentId}-onboard.json`);
|
|
1079
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
console.log("Failed to cache onboard data:", error.message);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Loads cached onboard data from disk (without credentials)
|
|
1087
|
+
*/
|
|
1088
|
+
loadCachedOnboardData() {
|
|
1089
|
+
if (!this.agentId)
|
|
1090
|
+
return null;
|
|
1091
|
+
try {
|
|
1092
|
+
const cachePath = path.join(os.homedir(), ".agentmesh", "context", `${this.agentId}-onboard.json`);
|
|
1093
|
+
if (!fs.existsSync(cachePath))
|
|
1094
|
+
return null;
|
|
1095
|
+
const raw = fs.readFileSync(cachePath, "utf-8");
|
|
1096
|
+
const data = JSON.parse(raw);
|
|
1097
|
+
return data;
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
965
1103
|
}
|
|
966
1104
|
//# sourceMappingURL=daemon.js.map
|