@agentmeshhq/agent 0.4.16 → 0.4.20

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/attach.test.d.ts +1 -0
  3. package/dist/__tests__/attach.test.js +200 -0
  4. package/dist/__tests__/attach.test.js.map +1 -0
  5. package/dist/__tests__/auth-guard.integration.test.js +1 -1
  6. package/dist/__tests__/auth-guard.integration.test.js.map +1 -1
  7. package/dist/__tests__/auth-guard.test.js +3 -3
  8. package/dist/__tests__/auth-guard.test.js.map +1 -1
  9. package/dist/__tests__/bootstrap.test.js +23 -0
  10. package/dist/__tests__/bootstrap.test.js.map +1 -1
  11. package/dist/__tests__/daemon-hub-resilience.test.js +2 -2
  12. package/dist/__tests__/daemon-hub-resilience.test.js.map +1 -1
  13. package/dist/__tests__/evicted-cleanup.test.js.map +1 -1
  14. package/dist/__tests__/injection-verify.test.d.ts +1 -0
  15. package/dist/__tests__/injection-verify.test.js +93 -0
  16. package/dist/__tests__/injection-verify.test.js.map +1 -0
  17. package/dist/__tests__/injector.test.js +124 -4
  18. package/dist/__tests__/injector.test.js.map +1 -1
  19. package/dist/__tests__/list.test.d.ts +1 -0
  20. package/dist/__tests__/list.test.js +62 -0
  21. package/dist/__tests__/list.test.js.map +1 -0
  22. package/dist/__tests__/opencode-serve.test.d.ts +1 -0
  23. package/dist/__tests__/opencode-serve.test.js +54 -0
  24. package/dist/__tests__/opencode-serve.test.js.map +1 -0
  25. package/dist/__tests__/opencode-session-policy.test.d.ts +1 -0
  26. package/dist/__tests__/opencode-session-policy.test.js +61 -0
  27. package/dist/__tests__/opencode-session-policy.test.js.map +1 -0
  28. package/dist/__tests__/opencode-session.test.d.ts +1 -0
  29. package/dist/__tests__/opencode-session.test.js +178 -0
  30. package/dist/__tests__/opencode-session.test.js.map +1 -0
  31. package/dist/__tests__/registry.register.test.js +16 -0
  32. package/dist/__tests__/registry.register.test.js.map +1 -1
  33. package/dist/__tests__/relay.test.d.ts +1 -0
  34. package/dist/__tests__/relay.test.js +136 -0
  35. package/dist/__tests__/relay.test.js.map +1 -0
  36. package/dist/__tests__/runner.test.js +17 -0
  37. package/dist/__tests__/runner.test.js.map +1 -1
  38. package/dist/__tests__/session-recovery.test.js +214 -11
  39. package/dist/__tests__/session-recovery.test.js.map +1 -1
  40. package/dist/__tests__/shared-resource-guards.test.js +1 -4
  41. package/dist/__tests__/shared-resource-guards.test.js.map +1 -1
  42. package/dist/__tests__/start-team-id.test.js +22 -0
  43. package/dist/__tests__/start-team-id.test.js.map +1 -1
  44. package/dist/__tests__/startup-diagnostics.test.d.ts +1 -0
  45. package/dist/__tests__/startup-diagnostics.test.js +250 -0
  46. package/dist/__tests__/startup-diagnostics.test.js.map +1 -0
  47. package/dist/__tests__/tmux-runtime.test.js +13 -0
  48. package/dist/__tests__/tmux-runtime.test.js.map +1 -1
  49. package/dist/__tests__/token-rejection-recovery.test.js +52 -0
  50. package/dist/__tests__/token-rejection-recovery.test.js.map +1 -1
  51. package/dist/__tests__/watcher-queue.test.d.ts +1 -0
  52. package/dist/__tests__/watcher-queue.test.js +90 -0
  53. package/dist/__tests__/watcher-queue.test.js.map +1 -0
  54. package/dist/__tests__/watcher-state.test.d.ts +1 -0
  55. package/dist/__tests__/watcher-state.test.js +159 -0
  56. package/dist/__tests__/watcher-state.test.js.map +1 -0
  57. package/dist/cli/attach.d.ts +1 -1
  58. package/dist/cli/attach.js +125 -2
  59. package/dist/cli/attach.js.map +1 -1
  60. package/dist/cli/auth.js.map +1 -1
  61. package/dist/cli/commands.d.ts +32 -0
  62. package/dist/cli/commands.js +165 -0
  63. package/dist/cli/commands.js.map +1 -0
  64. package/dist/cli/index.js +97 -4
  65. package/dist/cli/index.js.map +1 -1
  66. package/dist/cli/list.js +26 -2
  67. package/dist/cli/list.js.map +1 -1
  68. package/dist/cli/relay.d.ts +4 -0
  69. package/dist/cli/relay.js +165 -3
  70. package/dist/cli/relay.js.map +1 -1
  71. package/dist/cli/start.d.ts +9 -1
  72. package/dist/cli/start.js +8 -0
  73. package/dist/cli/start.js.map +1 -1
  74. package/dist/cli/status.js +21 -8
  75. package/dist/cli/status.js.map +1 -1
  76. package/dist/cli/test.js +12 -1
  77. package/dist/cli/test.js.map +1 -1
  78. package/dist/config/schema.d.ts +17 -1
  79. package/dist/core/auth-guard.js +2 -2
  80. package/dist/core/auth-guard.js.map +1 -1
  81. package/dist/core/chat-output-parser.d.ts +24 -0
  82. package/dist/core/chat-output-parser.js +150 -0
  83. package/dist/core/chat-output-parser.js.map +1 -0
  84. package/dist/core/chat-output-parser.test.d.ts +7 -0
  85. package/dist/core/chat-output-parser.test.js +151 -0
  86. package/dist/core/chat-output-parser.test.js.map +1 -0
  87. package/dist/core/daemon/bootstrap.d.ts +8 -0
  88. package/dist/core/daemon/bootstrap.js +6 -1
  89. package/dist/core/daemon/bootstrap.js.map +1 -1
  90. package/dist/core/daemon/crash-log.js +5 -0
  91. package/dist/core/daemon/crash-log.js.map +1 -1
  92. package/dist/core/daemon/injection-verify.d.ts +25 -0
  93. package/dist/core/daemon/injection-verify.js +94 -0
  94. package/dist/core/daemon/injection-verify.js.map +1 -0
  95. package/dist/core/daemon/roles.d.ts +2 -2
  96. package/dist/core/daemon/roles.js +3 -0
  97. package/dist/core/daemon/roles.js.map +1 -1
  98. package/dist/core/daemon/session-recovery.d.ts +18 -1
  99. package/dist/core/daemon/session-recovery.js +89 -5
  100. package/dist/core/daemon/session-recovery.js.map +1 -1
  101. package/dist/core/daemon/startup-diagnostics.d.ts +76 -0
  102. package/dist/core/daemon/startup-diagnostics.js +277 -0
  103. package/dist/core/daemon/startup-diagnostics.js.map +1 -0
  104. package/dist/core/daemon/state.d.ts +8 -0
  105. package/dist/core/daemon/state.js +8 -0
  106. package/dist/core/daemon/state.js.map +1 -1
  107. package/dist/core/daemon/tmux-session.d.ts +4 -0
  108. package/dist/core/daemon/tmux-session.js +9 -1
  109. package/dist/core/daemon/tmux-session.js.map +1 -1
  110. package/dist/core/daemon/watcher-loop.d.ts +27 -0
  111. package/dist/core/daemon/watcher-loop.js +134 -0
  112. package/dist/core/daemon/watcher-loop.js.map +1 -0
  113. package/dist/core/daemon/watcher-queue.d.ts +33 -0
  114. package/dist/core/daemon/watcher-queue.js +71 -0
  115. package/dist/core/daemon/watcher-queue.js.map +1 -0
  116. package/dist/core/daemon/watcher-state.d.ts +66 -0
  117. package/dist/core/daemon/watcher-state.js +151 -0
  118. package/dist/core/daemon/watcher-state.js.map +1 -0
  119. package/dist/core/daemon/workspace.js +10 -2
  120. package/dist/core/daemon/workspace.js.map +1 -1
  121. package/dist/core/daemon.d.ts +22 -1
  122. package/dist/core/daemon.js +289 -20
  123. package/dist/core/daemon.js.map +1 -1
  124. package/dist/core/handoff-sla.js +1 -1
  125. package/dist/core/handoff-sla.js.map +1 -1
  126. package/dist/core/injector.d.ts +2 -0
  127. package/dist/core/injector.js +227 -32
  128. package/dist/core/injector.js.map +1 -1
  129. package/dist/core/opencode-serve.d.ts +26 -0
  130. package/dist/core/opencode-serve.js +97 -0
  131. package/dist/core/opencode-serve.js.map +1 -0
  132. package/dist/core/opencode-session-policy.d.ts +10 -0
  133. package/dist/core/opencode-session-policy.js +10 -0
  134. package/dist/core/opencode-session-policy.js.map +1 -0
  135. package/dist/core/opencode-session.d.ts +12 -0
  136. package/dist/core/opencode-session.js +165 -0
  137. package/dist/core/opencode-session.js.map +1 -0
  138. package/dist/core/registry.d.ts +2 -1
  139. package/dist/core/registry.js +3 -2
  140. package/dist/core/registry.js.map +1 -1
  141. package/dist/core/runner/build.js +7 -31
  142. package/dist/core/runner/build.js.map +1 -1
  143. package/dist/core/runner/detect.js +2 -8
  144. package/dist/core/runner/detect.js.map +1 -1
  145. package/dist/core/runner/index.d.ts +3 -1
  146. package/dist/core/runner/index.js +2 -0
  147. package/dist/core/runner/index.js.map +1 -1
  148. package/dist/core/runner/kimi-models.d.ts +4 -0
  149. package/dist/core/runner/kimi-models.js +24 -0
  150. package/dist/core/runner/kimi-models.js.map +1 -0
  151. package/dist/core/runner/registry.d.ts +3 -0
  152. package/dist/core/runner/registry.js +75 -0
  153. package/dist/core/runner/registry.js.map +1 -0
  154. package/dist/core/runner/types.d.ts +17 -1
  155. package/dist/core/tmux-runtime.d.ts +2 -1
  156. package/dist/core/tmux-runtime.js +17 -1
  157. package/dist/core/tmux-runtime.js.map +1 -1
  158. package/dist/core/tmux.d.ts +4 -0
  159. package/dist/core/tmux.js +54 -11
  160. package/dist/core/tmux.js.map +1 -1
  161. package/dist/runtime/adapters/opencode.d.ts +63 -0
  162. package/dist/runtime/adapters/opencode.js +358 -0
  163. package/dist/runtime/adapters/opencode.js.map +1 -0
  164. package/dist/runtime/adapters/tmux-fallback.d.ts +23 -0
  165. package/dist/runtime/adapters/tmux-fallback.js +148 -0
  166. package/dist/runtime/adapters/tmux-fallback.js.map +1 -0
  167. package/dist/runtime/adapters/tmux-fallback.test.d.ts +4 -0
  168. package/dist/runtime/adapters/tmux-fallback.test.js +91 -0
  169. package/dist/runtime/adapters/tmux-fallback.test.js.map +1 -0
  170. package/dist/runtime/index.d.ts +146 -0
  171. package/dist/runtime/index.js +191 -0
  172. package/dist/runtime/index.js.map +1 -0
  173. package/dist/runtime/registry.d.ts +53 -0
  174. package/dist/runtime/registry.js +112 -0
  175. package/dist/runtime/registry.js.map +1 -0
  176. package/dist/runtime/registry.test.d.ts +4 -0
  177. package/dist/runtime/registry.test.js +69 -0
  178. package/dist/runtime/registry.test.js.map +1 -0
  179. package/dist/runtime/types.d.ts +158 -0
  180. package/dist/runtime/types.js +8 -0
  181. package/dist/runtime/types.js.map +1 -0
  182. package/package.json +11 -12
@@ -13,16 +13,21 @@ import { formatCrashLog } from "./daemon/crash-log.js";
13
13
  import { evaluateRestartState, filterActiveClaimsForAgent, filterCompletedHandoffsForAgent, formatRestartLifecycleLog, } from "./daemon/done-state-guard.js";
14
14
  import { cleanupGitAuth, setupGitAuth } from "./daemon/git-auth.js";
15
15
  import { getStuckDetail } from "./daemon/health-policy.js";
16
+ import { verifyInjection } from "./daemon/injection-verify.js";
16
17
  import { runLeadTick } from "./daemon/lead-loop.js";
17
18
  import { isKnownRole, ROLE_DAEMON_BEHAVIOUR, VALID_ROLES } from "./daemon/roles.js";
18
19
  import { writeSandboxOpencodeConfig } from "./daemon/sandbox-config.js";
19
20
  import { isRecoverableSessionFailure } from "./daemon/session-recovery.js";
20
21
  import { captureAgentChildPids, persistRunningState } from "./daemon/state.js";
21
22
  import { startTmuxRuntimeSession } from "./daemon/tmux-session.js";
23
+ import { handleWatcherWebSocketEvent, startWatcherLoop, } from "./daemon/watcher-loop.js";
24
+ import { WatcherTickQueue } from "./daemon/watcher-queue.js";
22
25
  import { configureGitIdentity, setupWorkspace, updateWorkspaceFromRemote, validatePushAccess, } from "./daemon/workspace.js";
23
26
  import { findPendingHandoffBreaches } from "./handoff-sla.js";
24
27
  import { Heartbeat } from "./heartbeat.js";
25
28
  import { handleWebSocketEvent, injectInboxItems, injectOnboardMessage, injectRestoredContext, injectStartupMessage, } from "./injector.js";
29
+ import { buildOpenCodeMetadata, fetchOpenCodeServerHealth, waitForOpenCodeServer, } from "./opencode-serve.js";
30
+ import { shouldPersistOpenCodeSession, shouldRestoreOpenCodeSession, } from "./opencode-session-policy.js";
26
31
  import { checkInbox, createClaim, createSelfAssignment, fetchAssignments, fetchHandoffsForAgent, fetchOnboard, fetchProjectByCode, getAgentAutonomyState, getHandoff, listClaims, patchAgentMetadata, registerAgent, releaseClaim, updateHandoffStatusWithRetry, } from "./registry.js";
27
32
  import { getRunnerDisplayName } from "./runner.js";
28
33
  import { DockerSandbox } from "./sandbox.js";
@@ -61,7 +66,12 @@ export class AgentDaemon {
61
66
  autoSetup;
62
67
  serveMode;
63
68
  servePort;
69
+ serveHostname;
70
+ servePublicUrl;
71
+ serveUsername;
72
+ servePasswordEnv;
64
73
  serveProcess = null;
74
+ serveHealth = null;
65
75
  sandboxMode;
66
76
  sandboxImage;
67
77
  sandboxCpu;
@@ -74,8 +84,11 @@ export class AgentDaemon {
74
84
  autonomous;
75
85
  healthCheckInterval = null;
76
86
  inboxPollInterval = null;
87
+ commandPollInterval = null;
77
88
  leadInterval = null;
78
89
  leadContext = null;
90
+ watcherLoopHandle = null;
91
+ watcherQueue = null;
79
92
  stopCleanupScheduler = null;
80
93
  authHealthWatcher = null;
81
94
  // Session resume tracking
@@ -85,12 +98,14 @@ export class AgentDaemon {
85
98
  stuckSince = null;
86
99
  lastPendingHandoffAlertAt = null;
87
100
  remoteAutomationPaused = false;
101
+ rateLimitDetected = false;
88
102
  lastAutonomyPolicyFetchAt = null;
89
103
  teamId;
90
104
  pendingClaimCreations = new Set();
91
105
  sessionRecoveryAttempts = 0;
92
106
  lastSessionRecoveryAt = null;
93
107
  initialInboxCheckComplete = false;
108
+ commandPollInFlight = false;
94
109
  constructor(options) {
95
110
  const boot = bootstrapDaemon(options);
96
111
  this.config = boot.config;
@@ -101,6 +116,10 @@ export class AgentDaemon {
101
116
  this.agentConfig = boot.agentConfig;
102
117
  this.serveMode = boot.serveMode;
103
118
  this.servePort = boot.servePort;
119
+ this.serveHostname = boot.serveHostname;
120
+ this.servePublicUrl = boot.servePublicUrl;
121
+ this.serveUsername = boot.serveUsername;
122
+ this.servePasswordEnv = boot.servePasswordEnv;
104
123
  this.sandboxMode = boot.sandboxMode;
105
124
  this.sandboxImage = boot.sandboxImage;
106
125
  this.sandboxCpu = boot.sandboxCpu;
@@ -165,6 +184,14 @@ export class AgentDaemon {
165
184
  // - neither → "system" (hidden background agent)
166
185
  const effectiveAgentType = this.agentConfig.agentType ??
167
186
  (this.isWorkerAgent ? "worker" : this.autonomous ? "autonomous" : "system");
187
+ const spawnRequestId = process.env.AGENTMESH_SPAWN_REQUEST_ID?.trim() || null;
188
+ const targetId = process.env.AGENTMESH_TARGET_ID?.trim() || null;
189
+ const registrationMetadata = spawnRequestId || targetId
190
+ ? {
191
+ ...(spawnRequestId ? { spawn_request_id: spawnRequestId } : {}),
192
+ ...(targetId ? { target_id: targetId } : {}),
193
+ }
194
+ : undefined;
168
195
  const registration = await registerAgent({
169
196
  url: this.config.hubUrl,
170
197
  apiKey: this.config.apiKey,
@@ -175,15 +202,11 @@ export class AgentDaemon {
175
202
  restoreContext: this.shouldRestoreContext,
176
203
  agentType: effectiveAgentType,
177
204
  teamId: this.teamId,
205
+ metadata: registrationMetadata,
178
206
  });
179
207
  this.agentId = registration.agentId;
180
208
  this.token = registration.token;
181
- // Best-effort: store role in hub agent metadata (once at startup, GH-829)
182
- if (this.projectRole && isKnownRole(this.projectRole)) {
183
- patchAgentMetadata(this.config.hubUrl, this.agentId, this.token, {
184
- role: this.projectRole,
185
- }).catch(() => { }); // fire-and-forget; non-fatal
186
- }
209
+ void this.publishDurableAgentMetadata();
187
210
  // Persist the agentId back to config.json so it survives state.json wipes.
188
211
  // On next startup, agentConfig.agentId is the fallback when existingState is absent.
189
212
  upsertAgentConfig({ ...this.agentConfig, agentId: this.agentId });
@@ -285,7 +308,10 @@ export class AgentDaemon {
285
308
  command: this.agentConfig.command,
286
309
  workdir: this.agentConfig.workdir,
287
310
  runnerEnv: this.runnerConfig.env,
311
+ runnerType: this.runnerConfig.type,
288
312
  shouldRestoreContext: this.shouldRestoreContext,
313
+ serveMode: this.serveMode,
314
+ sandboxMode: this.sandboxMode,
289
315
  autonomous: this.autonomous,
290
316
  });
291
317
  this._preStartSessionId = sessionStart.preStartSessionId;
@@ -310,6 +336,14 @@ export class AgentDaemon {
310
336
  sandboxContainer: this.sandbox?.getContainerName(),
311
337
  serveMode: this.serveMode,
312
338
  servePort: this.servePort,
339
+ serveHostname: this.serveHostname,
340
+ serveUrl: this.getServeLocalUrl(),
341
+ servePublicUrl: this.servePublicUrl,
342
+ serveAuthType: this.getServePassword() ? "basic" : "none",
343
+ serveUsername: this.serveUsername || (this.getServePassword() ? "opencode" : undefined),
344
+ servePasswordEnv: this.servePasswordEnv,
345
+ serveHealthy: this.serveHealth?.healthy,
346
+ serveVersion: this.serveHealth?.version,
313
347
  });
314
348
  // Track child PIDs for cleanup on restart/stop (tmux mode only — sandbox/serve manage their own)
315
349
  if (!this.sandboxMode && !this.serveMode) {
@@ -357,8 +391,14 @@ export class AgentDaemon {
357
391
  token: newToken,
358
392
  onMessage: (event) => {
359
393
  console.log(`[WS] Received event: ${event.type}`);
394
+ if (this.watcherQueue) {
395
+ handleWatcherWebSocketEvent(event, this.watcherQueue);
396
+ }
360
397
  this.autoAcceptHandoffFromEvent(event);
361
- handleWebSocketEvent(this.agentName, event);
398
+ handleWebSocketEvent(this.agentName, event, {
399
+ hubUrl: this.config.hubUrl,
400
+ token: this.token ?? undefined,
401
+ });
362
402
  },
363
403
  onConnect: () => {
364
404
  console.log("WebSocket reconnected with new token");
@@ -373,6 +413,7 @@ export class AgentDaemon {
373
413
  });
374
414
  this.ws.connect();
375
415
  }
416
+ void this.publishDurableAgentMetadata();
376
417
  },
377
418
  });
378
419
  this.heartbeat.start();
@@ -384,6 +425,10 @@ export class AgentDaemon {
384
425
  token: this.token,
385
426
  onMessage: (event) => {
386
427
  console.log(`[WS] Received event: ${event.type}`);
428
+ // Feed watcher queue before standard handlers (GH-883)
429
+ if (this.watcherQueue) {
430
+ handleWatcherWebSocketEvent(event, this.watcherQueue);
431
+ }
387
432
  this.autoAcceptHandoffFromEvent(event);
388
433
  handleWebSocketEvent(this.agentName, event, {
389
434
  hubUrl: this.config.hubUrl,
@@ -520,6 +565,7 @@ export class AgentDaemon {
520
565
  this.isRunning = true;
521
566
  // Start session health monitoring (every 60 seconds)
522
567
  this.startHealthMonitor();
568
+ this.startCommandPoller();
523
569
  // Start lead coordination loop for lead/coordinator roles (GH-824, GH-829)
524
570
  const roleBehaviour = isKnownRole(this.projectRole)
525
571
  ? ROLE_DAEMON_BEHAVIOUR[this.projectRole]
@@ -540,6 +586,19 @@ export class AgentDaemon {
540
586
  }, 120_000);
541
587
  console.log("[lead-loop] Lead coordination loop started (2-minute tick)");
542
588
  }
589
+ // Start LLM watcher loop for watcher role (GH-883)
590
+ if (roleBehaviour === "watcher-loop" && this.token) {
591
+ this.watcherQueue = new WatcherTickQueue();
592
+ this.watcherLoopHandle = startWatcherLoop({
593
+ hubUrl: this.config.hubUrl,
594
+ token: this.token,
595
+ workspace: this.config.workspace,
596
+ teamId: this.teamId ?? "",
597
+ agentName: this.agentName,
598
+ log: (msg) => console.log(msg),
599
+ }, this.watcherQueue);
600
+ console.log("[watcher-loop] LLM watcher loop started (2-minute tick)");
601
+ }
543
602
  // Start evicted-agent cleanup scheduler (GH-421)
544
603
  if (this.token) {
545
604
  this.stopCleanupScheduler = startCleanupScheduler({
@@ -579,6 +638,11 @@ Nudge agent:
579
638
  inboxItems.length === 0) {
580
639
  return inboxItems;
581
640
  }
641
+ // GH-887: Skip auto-accept if rate-limited
642
+ if (this.rateLimitDetected) {
643
+ console.log("[AUTO-ACCEPT] Skipping — agent is rate-limited");
644
+ return inboxItems;
645
+ }
582
646
  const accepted = new Set();
583
647
  for (const item of inboxItems) {
584
648
  if (item.type && item.type !== "handoff") {
@@ -590,6 +654,8 @@ Nudge agent:
590
654
  accepted.add(item.id);
591
655
  console.log(`[AUTO-ACCEPT] Accepted handoff ${item.id}`);
592
656
  await this.ensureClaimForHandoff(item.id, item.scope);
657
+ // GH-887: Verify the LLM actually started processing
658
+ void this.verifyHandoffInjection(item.id, item.scope);
593
659
  }
594
660
  }
595
661
  catch (error) {
@@ -598,6 +664,34 @@ Nudge agent:
598
664
  }
599
665
  return inboxItems.filter((item) => !accepted.has(item.id));
600
666
  }
667
+ /**
668
+ * GH-887: After accepting a handoff, verify the LLM acknowledged it.
669
+ * If rate-limited, pause auto-acceptance. If stuck after retries, log warning.
670
+ */
671
+ async verifyHandoffInjection(handoffId, scope) {
672
+ try {
673
+ const result = await verifyInjection(this.agentName, handoffId, scope);
674
+ if (result.rateLimited) {
675
+ this.rateLimitDetected = true;
676
+ console.warn(`[INJECT-VERIFY] Rate limit detected — pausing auto-accept for ${this.agentName}`);
677
+ // Clear rate limit after 5 minutes (check again then)
678
+ setTimeout(() => {
679
+ this.rateLimitDetected = false;
680
+ console.log("[INJECT-VERIFY] Rate limit cooldown expired — resuming auto-accept");
681
+ }, 300_000);
682
+ return;
683
+ }
684
+ if (result.verified) {
685
+ console.log(`[INJECT-VERIFY] Handoff ${handoffId} verified (${result.attempts} attempt${result.attempts > 1 ? "s" : ""})`);
686
+ }
687
+ else {
688
+ console.warn(`[INJECT-VERIFY] Handoff ${handoffId} NOT verified after ${result.attempts} attempts — LLM may be stuck`);
689
+ }
690
+ }
691
+ catch (error) {
692
+ console.warn(`[INJECT-VERIFY] Error verifying ${handoffId}: ${error.message}`);
693
+ }
694
+ }
601
695
  autoAcceptHandoffFromEvent(event) {
602
696
  if (!this.autoAcceptHandoffs || !this.token) {
603
697
  return;
@@ -836,9 +930,33 @@ Nudge agent:
836
930
  }
837
931
  }
838
932
  startHealthMonitor() {
839
- // Skip health monitoring for serve mode (no tmux session)
840
- if (this.serveMode)
933
+ if (this.serveMode) {
934
+ this.healthCheckInterval = setInterval(async () => {
935
+ if (!this.isRunning)
936
+ return;
937
+ try {
938
+ const health = await fetchOpenCodeServerHealth({
939
+ hostname: this.serveHostname,
940
+ port: this.servePort,
941
+ username: this.serveUsername,
942
+ password: this.getServePassword(),
943
+ });
944
+ await this.publishServeMetadata(health);
945
+ updateAgentInState(this.agentName, {
946
+ serveHealthy: health.healthy,
947
+ serveVersion: health.version,
948
+ });
949
+ }
950
+ catch (err) {
951
+ console.error(`[SERVE] Health check failed: ${err.message}`);
952
+ await this.publishServeMetadata({ healthy: false, version: this.serveHealth?.version });
953
+ updateAgentInState(this.agentName, {
954
+ serveHealthy: false,
955
+ });
956
+ }
957
+ }, 30000);
841
958
  return;
959
+ }
842
960
  // Start periodic auth healthcheck for opencode runners (Epic #470)
843
961
  if (this.runnerConfig.type === "opencode") {
844
962
  this.authHealthWatcher = startAuthHealthWatcher(this.agentName, (event) => {
@@ -911,6 +1029,58 @@ Nudge agent:
911
1029
  }, 5 * 60 * 1000); // Poll every 5 minutes
912
1030
  }
913
1031
  }
1032
+ startCommandPoller() {
1033
+ if (!this.token || !this.agentId || !this.isWorkerAgent) {
1034
+ return;
1035
+ }
1036
+ // Poll every 5s so commands created during WS disconnects are still picked up.
1037
+ this.commandPollInterval = setInterval(() => {
1038
+ void this.pollNextCommand();
1039
+ }, 5000);
1040
+ void this.pollNextCommand();
1041
+ }
1042
+ async pollNextCommand() {
1043
+ if (!this.isRunning || this.commandPollInFlight || !this.token || !this.agentId) {
1044
+ return;
1045
+ }
1046
+ this.commandPollInFlight = true;
1047
+ try {
1048
+ const params = new URLSearchParams({ target_agent_id: this.agentId });
1049
+ const res = await fetch(`${this.config.hubUrl}/api/v1/workspaces/${this.config.workspace}/commands/next?${params.toString()}`, {
1050
+ headers: {
1051
+ Authorization: `Bearer ${this.token}`,
1052
+ },
1053
+ });
1054
+ if (!res.ok) {
1055
+ if (res.status !== 404) {
1056
+ const body = await res.text();
1057
+ console.warn(`[command-poller] hub returned ${res.status}: ${body}`);
1058
+ }
1059
+ return;
1060
+ }
1061
+ const payload = (await res.json());
1062
+ if (!payload.command) {
1063
+ return;
1064
+ }
1065
+ handleWebSocketEvent(this.agentName, {
1066
+ type: "control.command",
1067
+ command_id: payload.command.command_id,
1068
+ workspace_id: payload.command.workspace_id,
1069
+ target_agent_id: payload.command.target_agent_id,
1070
+ command_type: payload.command.command_type,
1071
+ payload: payload.command.payload ?? {},
1072
+ }, {
1073
+ hubUrl: this.config.hubUrl,
1074
+ token: this.token ?? undefined,
1075
+ });
1076
+ }
1077
+ catch (err) {
1078
+ console.warn(`[command-poller] error: ${err.message}`);
1079
+ }
1080
+ finally {
1081
+ this.commandPollInFlight = false;
1082
+ }
1083
+ }
914
1084
  /**
915
1085
  * Handles session death - logs crash and attempts auto-restart
916
1086
  */
@@ -966,12 +1136,21 @@ Nudge agent:
966
1136
  clearInterval(this.inboxPollInterval);
967
1137
  this.inboxPollInterval = null;
968
1138
  }
1139
+ if (this.commandPollInterval) {
1140
+ clearInterval(this.commandPollInterval);
1141
+ this.commandPollInterval = null;
1142
+ }
1143
+ if (this.watcherLoopHandle) {
1144
+ this.watcherLoopHandle.stop();
1145
+ this.watcherLoopHandle = null;
1146
+ this.watcherQueue = null;
1147
+ }
969
1148
  }
970
1149
  async tryRecoverSession(reason) {
971
1150
  if (!this.isWorkerAgent || this.serveMode || this.sandboxMode) {
972
1151
  return false;
973
1152
  }
974
- if (!isRecoverableSessionFailure(reason)) {
1153
+ if (!isRecoverableSessionFailure(reason, this.agentName)) {
975
1154
  return false;
976
1155
  }
977
1156
  const now = Date.now();
@@ -990,7 +1169,10 @@ Nudge agent:
990
1169
  command: this.agentConfig.command,
991
1170
  workdir: this.agentConfig.workdir,
992
1171
  runnerEnv: this.runnerConfig.env,
1172
+ runnerType: this.runnerConfig.type,
993
1173
  shouldRestoreContext: false,
1174
+ serveMode: this.serveMode,
1175
+ sandboxMode: this.sandboxMode,
994
1176
  autonomous: this.autonomous,
995
1177
  });
996
1178
  this._preStartSessionId = sessionStart.preStartSessionId;
@@ -1069,6 +1251,10 @@ Nudge agent:
1069
1251
  clearInterval(this.inboxPollInterval);
1070
1252
  this.inboxPollInterval = null;
1071
1253
  }
1254
+ if (this.commandPollInterval) {
1255
+ clearInterval(this.commandPollInterval);
1256
+ this.commandPollInterval = null;
1257
+ }
1072
1258
  if (this.leadInterval) {
1073
1259
  clearInterval(this.leadInterval);
1074
1260
  this.leadInterval = null;
@@ -1137,9 +1323,10 @@ Nudge agent:
1137
1323
  * Replaces tmux with a direct HTTP server
1138
1324
  */
1139
1325
  async startServeMode() {
1140
- console.log(`Starting opencode serve mode on port ${this.servePort}...`);
1326
+ console.log(`Starting opencode serve mode on ${this.serveHostname}:${this.servePort}...`);
1141
1327
  const workdir = this.agentConfig.workdir || process.cwd();
1142
1328
  const agentDataDir = prepareOpenCodeRuntime(this.agentName);
1329
+ const servePassword = this.getServePassword();
1143
1330
  // Build environment for opencode serve
1144
1331
  const env = {
1145
1332
  ...process.env,
@@ -1148,8 +1335,17 @@ Nudge agent:
1148
1335
  AGENTMESH_AGENT_ID: this.agentId,
1149
1336
  XDG_DATA_HOME: agentDataDir,
1150
1337
  };
1338
+ if (this.serveUsername) {
1339
+ env.OPENCODE_SERVER_USERNAME = this.serveUsername;
1340
+ }
1341
+ if (servePassword) {
1342
+ env.OPENCODE_SERVER_PASSWORD = servePassword;
1343
+ if (!env.OPENCODE_SERVER_USERNAME) {
1344
+ env.OPENCODE_SERVER_USERNAME = this.serveUsername || "opencode";
1345
+ }
1346
+ }
1151
1347
  // Spawn opencode serve as a child process
1152
- this.serveProcess = spawn("opencode", ["serve", "--port", String(this.servePort), "--hostname", "0.0.0.0"], {
1348
+ this.serveProcess = spawn("opencode", ["serve", "--port", String(this.servePort), "--hostname", this.serveHostname], {
1153
1349
  cwd: workdir,
1154
1350
  env,
1155
1351
  stdio: ["ignore", "inherit", "inherit"],
@@ -1157,6 +1353,7 @@ Nudge agent:
1157
1353
  // Handle process exit
1158
1354
  this.serveProcess.on("exit", (code, signal) => {
1159
1355
  console.error(`opencode serve exited with code ${code}, signal ${signal}`);
1356
+ void this.publishServeMetadata({ healthy: false, version: this.serveHealth?.version });
1160
1357
  if (this.isRunning) {
1161
1358
  console.log("Restarting opencode serve in 5 seconds...");
1162
1359
  setTimeout(() => {
@@ -1169,19 +1366,81 @@ Nudge agent:
1169
1366
  this.serveProcess.on("error", (error) => {
1170
1367
  console.error("Failed to start opencode serve:", error);
1171
1368
  });
1172
- // Wait a moment for the server to start
1173
- await new Promise((resolve) => setTimeout(resolve, 2000));
1174
- console.log(`opencode serve started on http://0.0.0.0:${this.servePort}`);
1369
+ let health;
1370
+ try {
1371
+ health = await waitForOpenCodeServer({
1372
+ hostname: this.serveHostname,
1373
+ port: this.servePort,
1374
+ username: this.serveUsername,
1375
+ password: servePassword,
1376
+ }, { timeoutMs: 20000 });
1377
+ }
1378
+ catch (error) {
1379
+ const failedProcess = this.serveProcess;
1380
+ this.serveProcess = null;
1381
+ if (failedProcess && !failedProcess.killed) {
1382
+ failedProcess.kill("SIGTERM");
1383
+ }
1384
+ throw error;
1385
+ }
1386
+ this.serveHealth = health;
1387
+ updateAgentInState(this.agentName, {
1388
+ serveHealthy: health.healthy,
1389
+ serveVersion: health.version,
1390
+ });
1391
+ await this.publishServeMetadata(health);
1392
+ console.log(`opencode serve started on ${this.getServeLocalUrl()}`);
1175
1393
  // Store saved session ID for integration service reuse
1176
1394
  if (this.shouldRestoreContext && this.agentId) {
1177
1395
  const savedContext = loadContext(this.agentId);
1178
1396
  const savedSessionId = savedContext?.custom?.opencodeSessionId;
1179
- if (savedSessionId) {
1397
+ if (savedSessionId &&
1398
+ shouldRestoreOpenCodeSession({
1399
+ runnerType: this.runnerConfig.type,
1400
+ autonomous: this.autonomous,
1401
+ serveMode: this.serveMode,
1402
+ sandboxMode: this.sandboxMode,
1403
+ })) {
1180
1404
  console.log(`[SERVE] Saved OpenCode session available for reuse: ${savedSessionId}`);
1181
1405
  updateAgentInState(this.agentName, { opencodeSessionId: savedSessionId });
1182
1406
  }
1183
1407
  }
1184
1408
  }
1409
+ async publishServeMetadata(health) {
1410
+ if (!this.agentId || !this.token)
1411
+ return;
1412
+ this.serveHealth = health;
1413
+ await patchAgentMetadata(this.config.hubUrl, this.config.workspace, this.agentId, this.token, buildOpenCodeMetadata({
1414
+ hostname: this.serveHostname,
1415
+ port: this.servePort,
1416
+ publicUrl: this.servePublicUrl,
1417
+ configuredModel: this.runnerConfig.model,
1418
+ username: this.serveUsername,
1419
+ password: this.getServePassword(),
1420
+ }, health));
1421
+ }
1422
+ async publishDurableAgentMetadata() {
1423
+ if (!this.agentId || !this.token)
1424
+ return;
1425
+ if (this.projectRole && isKnownRole(this.projectRole)) {
1426
+ await patchAgentMetadata(this.config.hubUrl, this.config.workspace, this.agentId, this.token, {
1427
+ role: this.projectRole,
1428
+ });
1429
+ }
1430
+ if (this.serveMode && this.serveHealth) {
1431
+ await this.publishServeMetadata(this.serveHealth);
1432
+ }
1433
+ }
1434
+ getServeLocalUrl() {
1435
+ const effectiveHost = this.serveHostname === "0.0.0.0" ? "127.0.0.1" : this.serveHostname;
1436
+ return `http://${effectiveHost}:${this.servePort}`;
1437
+ }
1438
+ getServePassword() {
1439
+ if (this.servePasswordEnv) {
1440
+ return process.env[this.servePasswordEnv];
1441
+ }
1442
+ return process.env.OPENCODE_SERVER_PASSWORD;
1443
+ }
1185
1444
  /**
1186
1445
  * Starts agent in Docker sandbox mode
1187
1446
  * Provides filesystem isolation with only workspace mounted
@@ -1326,10 +1585,20 @@ Logs: docker logs ${containerName}
1326
1585
  };
1327
1586
  }
1328
1587
  // Capture OpenCode session ID for native resume on restart
1329
- const sessionId = getLatestSessionId(this.agentName);
1330
- if (sessionId) {
1331
- context.custom = { ...context.custom, opencodeSessionId: sessionId };
1332
- console.log(`[CONTEXT] Captured OpenCode session ID: ${sessionId}`);
1588
+ if (shouldPersistOpenCodeSession({
1589
+ runnerType: this.runnerConfig.type,
1590
+ autonomous: this.autonomous,
1591
+ serveMode: this.serveMode,
1592
+ sandboxMode: this.sandboxMode,
1593
+ })) {
1594
+ const sessionId = getLatestSessionId(this.agentName);
1595
+ if (sessionId) {
1596
+ context.custom = { ...context.custom, opencodeSessionId: sessionId };
1597
+ console.log(`[CONTEXT] Captured OpenCode session ID: ${sessionId}`);
1598
+ }
1599
+ }
1600
+ else if (context.custom?.opencodeSessionId) {
1601
+ context.custom = { ...context.custom, opencodeSessionId: undefined };
1333
1602
  }
1334
1603
  // Save updated context
1335
1604
  saveContext(context);