@agentmeshhq/agent 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +39 -0
  2. package/dist/__tests__/context-template.test.d.ts +4 -0
  3. package/dist/__tests__/context-template.test.js +233 -0
  4. package/dist/__tests__/context-template.test.js.map +1 -0
  5. package/dist/__tests__/loader.test.js +140 -28
  6. package/dist/__tests__/loader.test.js.map +1 -1
  7. package/dist/__tests__/no-respawn.test.d.ts +1 -0
  8. package/dist/__tests__/no-respawn.test.js +254 -0
  9. package/dist/__tests__/no-respawn.test.js.map +1 -0
  10. package/dist/__tests__/onboard.test.d.ts +5 -0
  11. package/dist/__tests__/onboard.test.js +341 -0
  12. package/dist/__tests__/onboard.test.js.map +1 -0
  13. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  14. package/dist/__tests__/orphan-process.test.js +286 -0
  15. package/dist/__tests__/orphan-process.test.js.map +1 -0
  16. package/dist/__tests__/runner.test.js +16 -0
  17. package/dist/__tests__/runner.test.js.map +1 -1
  18. package/dist/__tests__/shared-resource-guards.test.d.ts +7 -0
  19. package/dist/__tests__/shared-resource-guards.test.js +260 -0
  20. package/dist/__tests__/shared-resource-guards.test.js.map +1 -0
  21. package/dist/__tests__/watchdog.test.js +138 -12
  22. package/dist/__tests__/watchdog.test.js.map +1 -1
  23. package/dist/cli/status.js +11 -0
  24. package/dist/cli/status.js.map +1 -1
  25. package/dist/cli/stop.js +7 -2
  26. package/dist/cli/stop.js.map +1 -1
  27. package/dist/config/loader.d.ts +0 -4
  28. package/dist/config/loader.js +102 -42
  29. package/dist/config/loader.js.map +1 -1
  30. package/dist/config/schema.d.ts +6 -4
  31. package/dist/core/daemon/assignment-message.d.ts +12 -0
  32. package/dist/core/daemon/assignment-message.js +36 -0
  33. package/dist/core/daemon/assignment-message.js.map +1 -0
  34. package/dist/core/daemon/bootstrap.d.ts +35 -0
  35. package/dist/core/daemon/bootstrap.js +52 -0
  36. package/dist/core/daemon/bootstrap.js.map +1 -0
  37. package/dist/core/daemon/context-template.d.ts +11 -0
  38. package/dist/core/daemon/context-template.js +144 -0
  39. package/dist/core/daemon/context-template.js.map +1 -0
  40. package/dist/core/daemon/crash-log.d.ts +14 -0
  41. package/dist/core/daemon/crash-log.js +23 -0
  42. package/dist/core/daemon/crash-log.js.map +1 -0
  43. package/dist/core/daemon/git-auth.d.ts +18 -0
  44. package/dist/core/daemon/git-auth.js +88 -0
  45. package/dist/core/daemon/git-auth.js.map +1 -0
  46. package/dist/core/daemon/health-policy.d.ts +17 -0
  47. package/dist/core/daemon/health-policy.js +24 -0
  48. package/dist/core/daemon/health-policy.js.map +1 -0
  49. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  50. package/dist/core/daemon/sandbox-config.js +17 -0
  51. package/dist/core/daemon/sandbox-config.js.map +1 -0
  52. package/dist/core/daemon/state.d.ts +33 -0
  53. package/dist/core/daemon/state.js +78 -0
  54. package/dist/core/daemon/state.js.map +1 -0
  55. package/dist/core/daemon/tmux-session.d.ts +17 -0
  56. package/dist/core/daemon/tmux-session.js +34 -0
  57. package/dist/core/daemon/tmux-session.js.map +1 -0
  58. package/dist/core/daemon/workspace.d.ts +23 -0
  59. package/dist/core/daemon/workspace.js +90 -0
  60. package/dist/core/daemon/workspace.js.map +1 -0
  61. package/dist/core/daemon.d.ts +9 -12
  62. package/dist/core/daemon.js +293 -393
  63. package/dist/core/daemon.js.map +1 -1
  64. package/dist/core/injector.d.ts +5 -1
  65. package/dist/core/injector.js +83 -0
  66. package/dist/core/injector.js.map +1 -1
  67. package/dist/core/registry.d.ts +62 -0
  68. package/dist/core/registry.js +18 -0
  69. package/dist/core/registry.js.map +1 -1
  70. package/dist/core/runner/build.d.ts +9 -0
  71. package/dist/core/runner/build.js +53 -0
  72. package/dist/core/runner/build.js.map +1 -0
  73. package/dist/core/runner/detect.d.ts +5 -0
  74. package/dist/core/runner/detect.js +14 -0
  75. package/dist/core/runner/detect.js.map +1 -0
  76. package/dist/core/runner/index.d.ts +5 -0
  77. package/dist/core/runner/index.js +5 -0
  78. package/dist/core/runner/index.js.map +1 -0
  79. package/dist/core/runner/model.d.ts +5 -0
  80. package/dist/core/runner/model.js +7 -0
  81. package/dist/core/runner/model.js.map +1 -0
  82. package/dist/core/runner/opencode-models.d.ts +15 -0
  83. package/dist/core/runner/opencode-models.js +70 -0
  84. package/dist/core/runner/opencode-models.js.map +1 -0
  85. package/dist/core/runner/types.d.ts +19 -0
  86. package/dist/core/runner/types.js +8 -0
  87. package/dist/core/runner/types.js.map +1 -0
  88. package/dist/core/runner.d.ts +5 -47
  89. package/dist/core/runner.js +5 -167
  90. package/dist/core/runner.js.map +1 -1
  91. package/dist/core/tmux-runtime.d.ts +13 -0
  92. package/dist/core/tmux-runtime.js +72 -0
  93. package/dist/core/tmux-runtime.js.map +1 -0
  94. package/dist/core/tmux.d.ts +7 -1
  95. package/dist/core/tmux.js +75 -45
  96. package/dist/core/tmux.js.map +1 -1
  97. package/dist/core/watchdog.d.ts +18 -1
  98. package/dist/core/watchdog.js +78 -29
  99. package/dist/core/watchdog.js.map +1 -1
  100. package/package.json +24 -4
  101. package/src/__tests__/context.test.ts +0 -464
  102. package/src/__tests__/injector.test.ts +0 -29
  103. package/src/__tests__/jwt.test.ts +0 -112
  104. package/src/__tests__/loader.test.ts +0 -239
  105. package/src/__tests__/runner.test.ts +0 -104
  106. package/src/__tests__/sandbox.test.ts +0 -435
  107. package/src/__tests__/watchdog.test.ts +0 -368
  108. package/src/cli/attach.ts +0 -22
  109. package/src/cli/build.ts +0 -145
  110. package/src/cli/config.ts +0 -148
  111. package/src/cli/context.ts +0 -231
  112. package/src/cli/deploy.ts +0 -155
  113. package/src/cli/index.ts +0 -376
  114. package/src/cli/init.ts +0 -75
  115. package/src/cli/list.ts +0 -70
  116. package/src/cli/local.ts +0 -183
  117. package/src/cli/logs.ts +0 -64
  118. package/src/cli/migrate.ts +0 -212
  119. package/src/cli/nudge.ts +0 -81
  120. package/src/cli/restart.ts +0 -59
  121. package/src/cli/slack.ts +0 -70
  122. package/src/cli/start.ts +0 -118
  123. package/src/cli/status.ts +0 -91
  124. package/src/cli/stop.ts +0 -48
  125. package/src/cli/test.ts +0 -143
  126. package/src/cli/token.ts +0 -188
  127. package/src/cli/whoami.ts +0 -142
  128. package/src/config/loader.ts +0 -121
  129. package/src/config/schema.ts +0 -68
  130. package/src/context/handoff.ts +0 -122
  131. package/src/context/index.ts +0 -8
  132. package/src/context/schema.ts +0 -111
  133. package/src/context/storage.ts +0 -197
  134. package/src/core/daemon.ts +0 -1317
  135. package/src/core/heartbeat.ts +0 -129
  136. package/src/core/injector.ts +0 -292
  137. package/src/core/registry.ts +0 -159
  138. package/src/core/runner.ts +0 -225
  139. package/src/core/sandbox.ts +0 -547
  140. package/src/core/session-id.ts +0 -111
  141. package/src/core/tmux.ts +0 -405
  142. package/src/core/watchdog.ts +0 -238
  143. package/src/core/websocket.ts +0 -94
  144. package/src/index.ts +0 -10
  145. package/src/utils/jwt.ts +0 -87
  146. package/tsconfig.json +0 -8
  147. package/vitest.config.ts +0 -12
@@ -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 { addAgentToState, getAgentState, loadConfig, resetAgentRestartCount, updateAgentInState, } from "../config/loader.js";
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 { buildRunnerConfig, getRunnerDisplayName } from "./runner.js";
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, snapshotSessionId, waitForNewSessionId } from "./session-id.js";
13
- import { captureSessionContext, captureSessionOutput, createSession, destroySession, getSessionName, isSessionHealthy, sessionExists, updateSessionEnvironment, } from "./tmux.js";
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
- // Maximum number of auto-restart attempts
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
- // Auto-restart tracking
57
- restartCount = 0;
58
- lastStableTime = null;
63
+ // Stuck detection tracking
59
64
  stuckSince = null;
60
65
  nudgeSentAt = null;
61
66
  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;
67
+ const boot = bootstrapDaemon(options);
68
+ this.config = boot.config;
72
69
  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
- });
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, clean up stale state
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
- // 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
- }
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
- const sessionName = this.serveMode ? `serve:${this.servePort}` : getSessionName(this.agentName);
213
- addAgentToState({
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 - attempt restart
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 === "permission_blocked" || progress.status === "stuck") {
426
- await this.handleStuckAgent(progress);
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
- 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
- `;
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 (including session ID) before restart attempt
495
+ // Save context before marking as failed
474
496
  if (this.agentId) {
475
497
  this.saveAgentContext();
476
498
  }
477
- // Check if we can restart
478
- if (this.restartCount < MAX_RESTART_ATTEMPTS) {
479
- this.restartCount++;
480
- console.error(`[CRASH] Session died: ${reason}. Attempting restart (${this.restartCount}/${MAX_RESTART_ATTEMPTS})...`);
481
- updateAgentInState(this.agentName, {
482
- restartCount: this.restartCount,
483
- lastRestartAt: timestamp,
484
- status: "running",
485
- });
486
- try {
487
- await this.restartSession();
488
- console.log(`[RESTART] Agent restarted successfully`);
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
- async handleStuckAgent(progress) {
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.details || progress.blockedOn || "no activity"}`);
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
- // Only nudge worker agents - others restart immediately
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.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.";
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
- const timeSinceNudge = now.getTime() - this.nudgeSentAt.getTime();
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
- // 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
- }
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
- console.log(`Using workdir from project settings: ${assignmentWithWorkdir.project.workdir}`);
958
- this.agentConfig.workdir = assignmentWithWorkdir.project.workdir;
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 = this.setupWorkspace(expandedPath, repo.url, repo.default_branch, repoAssignment.project.name);
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
- ⚠️ 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
- `);
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
- * Sets up workspace by cloning repository or using existing clone
1014
- * Returns the absolute path to the workspace
930
+ * Ensures the sandbox OpenCode config exists
931
+ * Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
1015
932
  */
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`);
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
- execSync(`git clone --branch ${defaultBranch} "${repoUrl}" "${workspacePath}"`, {
1051
- stdio: "inherit",
1052
- });
1053
- console.log(`\n✓ Repository cloned successfully`);
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.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);
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
- * Ensures the sandbox OpenCode config exists
1066
- * Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
969
+ * Loads cached onboard data from disk (without credentials)
1067
970
  */
1068
- ensureSandboxOpencodeConfig() {
1069
- const configDir = path.dirname(SANDBOX_OPENCODE_CONFIG_PATH);
1070
- if (!fs.existsSync(configDir)) {
1071
- fs.mkdirSync(configDir, { recursive: true });
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
- // 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;
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