@agentmeshhq/agent 0.2.1 → 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 (62) hide show
  1. package/dist/__tests__/context-template.test.d.ts +4 -0
  2. package/dist/__tests__/context-template.test.js +233 -0
  3. package/dist/__tests__/context-template.test.js.map +1 -0
  4. package/dist/__tests__/loader.test.js +140 -28
  5. package/dist/__tests__/loader.test.js.map +1 -1
  6. package/dist/__tests__/no-respawn.test.d.ts +1 -0
  7. package/dist/__tests__/no-respawn.test.js +254 -0
  8. package/dist/__tests__/no-respawn.test.js.map +1 -0
  9. package/dist/__tests__/onboard.test.d.ts +5 -0
  10. package/dist/__tests__/onboard.test.js +341 -0
  11. package/dist/__tests__/onboard.test.js.map +1 -0
  12. package/dist/__tests__/shared-resource-guards.test.d.ts +7 -0
  13. package/dist/__tests__/shared-resource-guards.test.js +260 -0
  14. package/dist/__tests__/shared-resource-guards.test.js.map +1 -0
  15. package/dist/cli/inbox.d.ts +5 -0
  16. package/dist/cli/inbox.js +123 -0
  17. package/dist/cli/inbox.js.map +1 -0
  18. package/dist/cli/index.js +0 -0
  19. package/dist/cli/issue.d.ts +42 -0
  20. package/dist/cli/issue.js +297 -0
  21. package/dist/cli/issue.js.map +1 -0
  22. package/dist/cli/ready.d.ts +5 -0
  23. package/dist/cli/ready.js +131 -0
  24. package/dist/cli/ready.js.map +1 -0
  25. package/dist/cli/sync.d.ts +8 -0
  26. package/dist/cli/sync.js +154 -0
  27. package/dist/cli/sync.js.map +1 -0
  28. package/dist/config/loader.d.ts +0 -4
  29. package/dist/config/loader.js +102 -42
  30. package/dist/config/loader.js.map +1 -1
  31. package/dist/config/schema.d.ts +2 -2
  32. package/dist/core/daemon/context-template.d.ts +11 -0
  33. package/dist/core/daemon/context-template.js +144 -0
  34. package/dist/core/daemon/context-template.js.map +1 -0
  35. package/dist/core/daemon/crash-log.d.ts +0 -2
  36. package/dist/core/daemon/crash-log.js +0 -1
  37. package/dist/core/daemon/crash-log.js.map +1 -1
  38. package/dist/core/daemon/git-auth.d.ts +18 -0
  39. package/dist/core/daemon/git-auth.js +88 -0
  40. package/dist/core/daemon/git-auth.js.map +1 -0
  41. package/dist/core/daemon/health-policy.d.ts +0 -4
  42. package/dist/core/daemon/health-policy.js +0 -8
  43. package/dist/core/daemon/health-policy.js.map +1 -1
  44. package/dist/core/daemon/state.js +1 -0
  45. package/dist/core/daemon/state.js.map +1 -1
  46. package/dist/core/daemon/workspace.d.ts +13 -0
  47. package/dist/core/daemon/workspace.js +39 -0
  48. package/dist/core/daemon/workspace.js.map +1 -1
  49. package/dist/core/daemon.d.ts +9 -6
  50. package/dist/core/daemon.js +206 -185
  51. package/dist/core/daemon.js.map +1 -1
  52. package/dist/core/injector.d.ts +5 -1
  53. package/dist/core/injector.js +77 -0
  54. package/dist/core/injector.js.map +1 -1
  55. package/dist/core/issue-cache.d.ts +44 -0
  56. package/dist/core/issue-cache.js +75 -0
  57. package/dist/core/issue-cache.js.map +1 -0
  58. package/dist/core/registry.d.ts +62 -0
  59. package/dist/core/registry.js +18 -0
  60. package/dist/core/registry.js.map +1 -1
  61. package/package.json +12 -11
  62. package/LICENSE +0 -21
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/daemon/workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAS7B;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAErE,OAAO,CAAC,GAAG,CAAC,oFAAoF,CAAC,CAAC;IAClG,OAAO,CAAC,GAAG,CAAC,2CAA2C,WAAW,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,oFAAoF,CAAC,CAAC;IAElG,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,+BAA+B,aAAa,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAEzC,IAAI,CAAC;YACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,gBAAgB,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACpF,QAAQ,CAAC,mBAAmB,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACvF,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,iCAAkC,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YAC1E,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;QAChD,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,WAAW,aAAa,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,aAAa,aAAa,IAAI,CAAC,CAAC;IAE5C,IAAI,CAAC;QACH,QAAQ,CAAC,sBAAsB,aAAa,KAAK,OAAO,MAAM,aAAa,GAAG,EAAE;YAC9E,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mCAAoC,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,OAAO,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,aAAa,IAAI,CAAC,CAAC;IACrD,OAAO,aAAa,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/daemon/workspace.ts"],"names":[],"mappings":"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"}
@@ -44,12 +44,11 @@ export declare class AgentDaemon {
44
44
  private sandboxImage;
45
45
  private sandboxCpu;
46
46
  private sandboxMemory;
47
+ private onboardData;
47
48
  private sandbox;
48
49
  private healthCheckInterval;
49
50
  private _preStartSessionId;
50
51
  private _attemptedResumeSessionId;
51
- private restartCount;
52
- private lastStableTime;
53
52
  private stuckSince;
54
53
  private nudgeSentAt;
55
54
  constructor(options: DaemonOptions);
@@ -67,10 +66,6 @@ export declare class AgentDaemon {
67
66
  * Handles stuck agent - sends nudge first, then restarts if still stuck
68
67
  */
69
68
  private handleStuckAgent;
70
- /**
71
- * Restarts the agent session (sandbox or non-sandbox)
72
- */
73
- private restartSession;
74
69
  stop(): Promise<void>;
75
70
  /**
76
71
  * Starts opencode serve mode (for Integration Service)
@@ -100,4 +95,12 @@ export declare class AgentDaemon {
100
95
  * Creates ~/.agentmesh/opencode-sandbox.json with permissive permissions and model
101
96
  */
102
97
  private ensureSandboxOpencodeConfig;
98
+ /**
99
+ * Caches onboard data to disk for offline fallback
100
+ */
101
+ private cacheOnboardData;
102
+ /**
103
+ * Loads cached onboard data from disk (without credentials)
104
+ */
105
+ private loadCachedOnboardData;
103
106
  }
@@ -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, 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
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 { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, shouldResetRestartCount, } from "./daemon/health-policy.js";
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, fetchAssignments, fetchOnboard, registerAgent, } from "./registry.js";
18
20
  import { getRunnerDisplayName } from "./runner.js";
19
21
  import { DockerSandbox } from "./sandbox.js";
20
- import { getLatestSessionId, snapshotSessionId, waitForNewSessionId } from "./session-id.js";
21
- import { captureSessionContext, captureSessionOutput, createSession, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
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
- // Maximum number of auto-restart attempts
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,13 @@ export class AgentDaemon {
56
54
  sandboxImage;
57
55
  sandboxCpu;
58
56
  sandboxMemory;
57
+ onboardData = null;
59
58
  sandbox = null;
60
59
  healthCheckInterval = null;
61
60
  // Session resume tracking
62
61
  _preStartSessionId;
63
62
  _attemptedResumeSessionId;
64
- // Auto-restart tracking
65
- restartCount = 0;
66
- lastStableTime = null;
63
+ // Stuck detection tracking
67
64
  stuckSince = null;
68
65
  nudgeSentAt = null;
69
66
  constructor(options) {
@@ -113,9 +110,6 @@ export class AgentDaemon {
113
110
  console.log(`Cleaned up ${cleaned} orphan container(s)`);
114
111
  }
115
112
  }
116
- // Reset restart count on manual start
117
- this.restartCount = 0;
118
- this.lastStableTime = new Date();
119
113
  // Register with hub first (needed for assignment check)
120
114
  console.log("Registering with AgentMesh hub...");
121
115
  console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
@@ -139,8 +133,65 @@ export class AgentDaemon {
139
133
  else {
140
134
  console.log(`Registered as: ${this.agentId}`);
141
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
+ }
142
165
  // Check assignments and auto-setup workdir if needed (before creating tmux session)
143
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
+ }
144
195
  // Choose runtime mode: sandbox > serve > tmux
145
196
  if (this.sandboxMode) {
146
197
  await this.startSandboxMode();
@@ -277,6 +328,20 @@ export class AgentDaemon {
277
328
  console.error("Failed to check inbox:", error);
278
329
  injectStartupMessage(this.agentName, 0);
279
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
+ }
280
345
  // Restore context from previous session
281
346
  if (this.shouldRestoreContext && this.agentId) {
282
347
  console.log("Checking for previous context...");
@@ -368,17 +433,11 @@ Nudge agent:
368
433
  this.healthCheckInterval = setInterval(async () => {
369
434
  if (!this.isRunning)
370
435
  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
436
  // For sandbox mode, pass container name so health check looks inside container
378
437
  const containerName = this.sandboxMode ? this.sandbox?.getContainerName() : undefined;
379
438
  const health = isSessionHealthy(this.agentName, containerName);
380
439
  if (!health.healthy) {
381
- // Session died - attempt restart
440
+ // Session died - log and mark as failed (no auto-restart)
382
441
  await this.handleSessionDeath(health.reason || "unknown", logDir);
383
442
  return;
384
443
  }
@@ -395,7 +454,7 @@ Nudge agent:
395
454
  console.log(`[HEALTH] Agent is waiting for human input: ${progress.details}`);
396
455
  }
397
456
  else if (progress.status === "permission_blocked" || progress.status === "stuck") {
398
- await this.handleStuckAgent(progress);
457
+ this.handleStuckAgent(progress);
399
458
  }
400
459
  else if (progress.status === "active") {
401
460
  // Agent is working - reset stuck tracking
@@ -405,7 +464,6 @@ Nudge agent:
405
464
  this.nudgeSentAt = null;
406
465
  updateAgentInState(this.agentName, { stuckSince: undefined, status: "running" });
407
466
  }
408
- this.lastStableTime = new Date();
409
467
  }
410
468
  }, 60000); // Check every 60 seconds
411
469
  }
@@ -428,57 +486,34 @@ Nudge agent:
428
486
  agentName: this.agentName,
429
487
  agentId: this.agentId,
430
488
  reason,
431
- restartCount: this.restartCount,
432
- maxRestartAttempts: MAX_RESTART_ATTEMPTS,
433
489
  sandboxLabel: this.sandboxMode ? this.sandbox?.getContainerName() || "sandbox" : "none",
434
490
  workdir: this.agentConfig.workdir,
435
491
  model: this.runnerConfig.model,
436
492
  lastOutput,
437
493
  });
438
494
  fs.appendFileSync(logFile, crashLog);
439
- // Save context (including session ID) before restart attempt
495
+ // Save context before marking as failed
440
496
  if (this.agentId) {
441
497
  this.saveAgentContext();
442
498
  }
443
- // Check if we can restart
444
- if (this.restartCount < MAX_RESTART_ATTEMPTS) {
445
- this.restartCount++;
446
- console.error(`[CRASH] Session died: ${reason}. Attempting restart (${this.restartCount}/${MAX_RESTART_ATTEMPTS})...`);
447
- updateAgentInState(this.agentName, {
448
- restartCount: this.restartCount,
449
- lastRestartAt: timestamp,
450
- status: "running",
451
- });
452
- try {
453
- await this.restartSession();
454
- console.log(`[RESTART] Agent restarted successfully`);
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
- }
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;
476
511
  }
477
512
  }
478
513
  /**
479
514
  * Handles stuck agent - sends nudge first, then restarts if still stuck
480
515
  */
481
- async handleStuckAgent(progress) {
516
+ handleStuckAgent(progress) {
482
517
  const now = new Date();
483
518
  if (!this.stuckSince) {
484
519
  // First detection of stuck state
@@ -489,7 +524,7 @@ Nudge agent:
489
524
  status: "stuck",
490
525
  });
491
526
  }
492
- // Only nudge worker agents - others restart immediately
527
+ // Nudge worker agents don't escalate to restart
493
528
  if (this.isWorkerAgent) {
494
529
  // If we haven't sent a nudge yet, send one
495
530
  if (!this.nudgeSentAt) {
@@ -510,124 +545,9 @@ Nudge agent:
510
545
  // Still waiting for agent to respond to nudge
511
546
  return;
512
547
  }
548
+ // Nudge grace period expired — log warning but do NOT restart
549
+ console.log(`[HEALTH] Agent still stuck after nudge. Manual intervention required.`);
513
550
  }
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
551
  }
632
552
  async stop() {
633
553
  console.log(`\nStopping agent: ${this.agentName}`);
@@ -642,6 +562,12 @@ Nudge agent:
642
562
  console.log("Saving agent context...");
643
563
  this.saveAgentContext();
644
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
+ }
645
571
  // Stop heartbeat
646
572
  if (this.heartbeat) {
647
573
  this.heartbeat.stop();
@@ -912,8 +838,51 @@ Logs: docker logs ${containerName}
912
838
  if (!this.agentConfig.workdir) {
913
839
  const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
914
840
  if (assignmentWithWorkdir?.project.workdir) {
915
- console.log(`Using workdir from project settings: ${assignmentWithWorkdir.project.workdir}`);
916
- 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
+ }
917
886
  return;
918
887
  }
919
888
  // No project.workdir set, check if we have a repo assignment
@@ -930,6 +899,13 @@ Logs: docker logs ${containerName}
930
899
  defaultBranch: repo.default_branch,
931
900
  projectName: repoAssignment.project.name,
932
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);
933
909
  return;
934
910
  }
935
911
  console.error(renderMissingWorkdirMessage({
@@ -962,5 +938,50 @@ Logs: docker logs ${containerName}
962
938
  });
963
939
  console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
964
940
  }
941
+ /**
942
+ * Caches onboard data to disk for offline fallback
943
+ */
944
+ cacheOnboardData() {
945
+ if (!this.onboardData || !this.agentId)
946
+ return;
947
+ try {
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));
963
+ }
964
+ catch (error) {
965
+ console.log("Failed to cache onboard data:", error.message);
966
+ }
967
+ }
968
+ /**
969
+ * Loads cached onboard data from disk (without credentials)
970
+ */
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;
981
+ }
982
+ catch {
983
+ return null;
984
+ }
985
+ }
965
986
  }
966
987
  //# sourceMappingURL=daemon.js.map