@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.
- package/LICENSE +21 -0
- package/dist/__tests__/attach.test.d.ts +1 -0
- package/dist/__tests__/attach.test.js +200 -0
- package/dist/__tests__/attach.test.js.map +1 -0
- package/dist/__tests__/auth-guard.integration.test.js +1 -1
- package/dist/__tests__/auth-guard.integration.test.js.map +1 -1
- package/dist/__tests__/auth-guard.test.js +3 -3
- package/dist/__tests__/auth-guard.test.js.map +1 -1
- package/dist/__tests__/bootstrap.test.js +23 -0
- package/dist/__tests__/bootstrap.test.js.map +1 -1
- package/dist/__tests__/daemon-hub-resilience.test.js +2 -2
- package/dist/__tests__/daemon-hub-resilience.test.js.map +1 -1
- package/dist/__tests__/evicted-cleanup.test.js.map +1 -1
- package/dist/__tests__/injection-verify.test.d.ts +1 -0
- package/dist/__tests__/injection-verify.test.js +93 -0
- package/dist/__tests__/injection-verify.test.js.map +1 -0
- package/dist/__tests__/injector.test.js +124 -4
- package/dist/__tests__/injector.test.js.map +1 -1
- package/dist/__tests__/list.test.d.ts +1 -0
- package/dist/__tests__/list.test.js +62 -0
- package/dist/__tests__/list.test.js.map +1 -0
- package/dist/__tests__/opencode-serve.test.d.ts +1 -0
- package/dist/__tests__/opencode-serve.test.js +54 -0
- package/dist/__tests__/opencode-serve.test.js.map +1 -0
- package/dist/__tests__/opencode-session-policy.test.d.ts +1 -0
- package/dist/__tests__/opencode-session-policy.test.js +61 -0
- package/dist/__tests__/opencode-session-policy.test.js.map +1 -0
- package/dist/__tests__/opencode-session.test.d.ts +1 -0
- package/dist/__tests__/opencode-session.test.js +178 -0
- package/dist/__tests__/opencode-session.test.js.map +1 -0
- package/dist/__tests__/registry.register.test.js +16 -0
- package/dist/__tests__/registry.register.test.js.map +1 -1
- package/dist/__tests__/relay.test.d.ts +1 -0
- package/dist/__tests__/relay.test.js +136 -0
- package/dist/__tests__/relay.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +17 -0
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/session-recovery.test.js +214 -11
- package/dist/__tests__/session-recovery.test.js.map +1 -1
- package/dist/__tests__/shared-resource-guards.test.js +1 -4
- package/dist/__tests__/shared-resource-guards.test.js.map +1 -1
- package/dist/__tests__/start-team-id.test.js +22 -0
- package/dist/__tests__/start-team-id.test.js.map +1 -1
- package/dist/__tests__/startup-diagnostics.test.d.ts +1 -0
- package/dist/__tests__/startup-diagnostics.test.js +250 -0
- package/dist/__tests__/startup-diagnostics.test.js.map +1 -0
- package/dist/__tests__/tmux-runtime.test.js +13 -0
- package/dist/__tests__/tmux-runtime.test.js.map +1 -1
- package/dist/__tests__/token-rejection-recovery.test.js +52 -0
- package/dist/__tests__/token-rejection-recovery.test.js.map +1 -1
- package/dist/__tests__/watcher-queue.test.d.ts +1 -0
- package/dist/__tests__/watcher-queue.test.js +90 -0
- package/dist/__tests__/watcher-queue.test.js.map +1 -0
- package/dist/__tests__/watcher-state.test.d.ts +1 -0
- package/dist/__tests__/watcher-state.test.js +159 -0
- package/dist/__tests__/watcher-state.test.js.map +1 -0
- package/dist/cli/attach.d.ts +1 -1
- package/dist/cli/attach.js +125 -2
- package/dist/cli/attach.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/commands.d.ts +32 -0
- package/dist/cli/commands.js +165 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.js +97 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/list.js +26 -2
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/relay.d.ts +4 -0
- package/dist/cli/relay.js +165 -3
- package/dist/cli/relay.js.map +1 -1
- package/dist/cli/start.d.ts +9 -1
- package/dist/cli/start.js +8 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/status.js +21 -8
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/test.js +12 -1
- package/dist/cli/test.js.map +1 -1
- package/dist/config/schema.d.ts +17 -1
- package/dist/core/auth-guard.js +2 -2
- package/dist/core/auth-guard.js.map +1 -1
- package/dist/core/chat-output-parser.d.ts +24 -0
- package/dist/core/chat-output-parser.js +150 -0
- package/dist/core/chat-output-parser.js.map +1 -0
- package/dist/core/chat-output-parser.test.d.ts +7 -0
- package/dist/core/chat-output-parser.test.js +151 -0
- package/dist/core/chat-output-parser.test.js.map +1 -0
- package/dist/core/daemon/bootstrap.d.ts +8 -0
- package/dist/core/daemon/bootstrap.js +6 -1
- package/dist/core/daemon/bootstrap.js.map +1 -1
- package/dist/core/daemon/crash-log.js +5 -0
- package/dist/core/daemon/crash-log.js.map +1 -1
- package/dist/core/daemon/injection-verify.d.ts +25 -0
- package/dist/core/daemon/injection-verify.js +94 -0
- package/dist/core/daemon/injection-verify.js.map +1 -0
- package/dist/core/daemon/roles.d.ts +2 -2
- package/dist/core/daemon/roles.js +3 -0
- package/dist/core/daemon/roles.js.map +1 -1
- package/dist/core/daemon/session-recovery.d.ts +18 -1
- package/dist/core/daemon/session-recovery.js +89 -5
- package/dist/core/daemon/session-recovery.js.map +1 -1
- package/dist/core/daemon/startup-diagnostics.d.ts +76 -0
- package/dist/core/daemon/startup-diagnostics.js +277 -0
- package/dist/core/daemon/startup-diagnostics.js.map +1 -0
- package/dist/core/daemon/state.d.ts +8 -0
- package/dist/core/daemon/state.js +8 -0
- package/dist/core/daemon/state.js.map +1 -1
- package/dist/core/daemon/tmux-session.d.ts +4 -0
- package/dist/core/daemon/tmux-session.js +9 -1
- package/dist/core/daemon/tmux-session.js.map +1 -1
- package/dist/core/daemon/watcher-loop.d.ts +27 -0
- package/dist/core/daemon/watcher-loop.js +134 -0
- package/dist/core/daemon/watcher-loop.js.map +1 -0
- package/dist/core/daemon/watcher-queue.d.ts +33 -0
- package/dist/core/daemon/watcher-queue.js +71 -0
- package/dist/core/daemon/watcher-queue.js.map +1 -0
- package/dist/core/daemon/watcher-state.d.ts +66 -0
- package/dist/core/daemon/watcher-state.js +151 -0
- package/dist/core/daemon/watcher-state.js.map +1 -0
- package/dist/core/daemon/workspace.js +10 -2
- package/dist/core/daemon/workspace.js.map +1 -1
- package/dist/core/daemon.d.ts +22 -1
- package/dist/core/daemon.js +289 -20
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/handoff-sla.js +1 -1
- package/dist/core/handoff-sla.js.map +1 -1
- package/dist/core/injector.d.ts +2 -0
- package/dist/core/injector.js +227 -32
- package/dist/core/injector.js.map +1 -1
- package/dist/core/opencode-serve.d.ts +26 -0
- package/dist/core/opencode-serve.js +97 -0
- package/dist/core/opencode-serve.js.map +1 -0
- package/dist/core/opencode-session-policy.d.ts +10 -0
- package/dist/core/opencode-session-policy.js +10 -0
- package/dist/core/opencode-session-policy.js.map +1 -0
- package/dist/core/opencode-session.d.ts +12 -0
- package/dist/core/opencode-session.js +165 -0
- package/dist/core/opencode-session.js.map +1 -0
- package/dist/core/registry.d.ts +2 -1
- package/dist/core/registry.js +3 -2
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner/build.js +7 -31
- package/dist/core/runner/build.js.map +1 -1
- package/dist/core/runner/detect.js +2 -8
- package/dist/core/runner/detect.js.map +1 -1
- package/dist/core/runner/index.d.ts +3 -1
- package/dist/core/runner/index.js +2 -0
- package/dist/core/runner/index.js.map +1 -1
- package/dist/core/runner/kimi-models.d.ts +4 -0
- package/dist/core/runner/kimi-models.js +24 -0
- package/dist/core/runner/kimi-models.js.map +1 -0
- package/dist/core/runner/registry.d.ts +3 -0
- package/dist/core/runner/registry.js +75 -0
- package/dist/core/runner/registry.js.map +1 -0
- package/dist/core/runner/types.d.ts +17 -1
- package/dist/core/tmux-runtime.d.ts +2 -1
- package/dist/core/tmux-runtime.js +17 -1
- package/dist/core/tmux-runtime.js.map +1 -1
- package/dist/core/tmux.d.ts +4 -0
- package/dist/core/tmux.js +54 -11
- package/dist/core/tmux.js.map +1 -1
- package/dist/runtime/adapters/opencode.d.ts +63 -0
- package/dist/runtime/adapters/opencode.js +358 -0
- package/dist/runtime/adapters/opencode.js.map +1 -0
- package/dist/runtime/adapters/tmux-fallback.d.ts +23 -0
- package/dist/runtime/adapters/tmux-fallback.js +148 -0
- package/dist/runtime/adapters/tmux-fallback.js.map +1 -0
- package/dist/runtime/adapters/tmux-fallback.test.d.ts +4 -0
- package/dist/runtime/adapters/tmux-fallback.test.js +91 -0
- package/dist/runtime/adapters/tmux-fallback.test.js.map +1 -0
- package/dist/runtime/index.d.ts +146 -0
- package/dist/runtime/index.js +191 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/registry.d.ts +53 -0
- package/dist/runtime/registry.js +112 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime/registry.test.d.ts +4 -0
- package/dist/runtime/registry.test.js +69 -0
- package/dist/runtime/registry.test.js.map +1 -0
- package/dist/runtime/types.d.ts +158 -0
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/package.json +11 -12
package/dist/core/daemon.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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);
|