agent-relay 1.3.1 → 1.3.3
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/.trajectories/active/traj_3yx9dy148mge.json +42 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
- package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
- package/.trajectories/index.json +140 -1
- package/README.md +23 -9
- package/TRAIL_GIT_AUTH_FIX.md +113 -0
- package/deploy/workspace/codex.config.toml +1 -1
- package/deploy/workspace/entrypoint.sh +20 -79
- package/deploy/workspace/gh-relay +156 -0
- package/deploy/workspace/git-credential-relay +5 -1
- package/dist/bridge/multi-project-client.js +13 -10
- package/dist/bridge/spawner.d.ts +2 -0
- package/dist/bridge/spawner.js +58 -76
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.d.ts +8 -6
- package/dist/cli/index.js +297 -30
- package/dist/cloud/api/admin.js +16 -3
- package/dist/cloud/api/codex-auth-helper.js +28 -8
- package/dist/cloud/api/consensus.d.ts +13 -0
- package/dist/cloud/api/consensus.js +259 -0
- package/dist/cloud/api/daemons.js +205 -1
- package/dist/cloud/api/git.js +37 -7
- package/dist/cloud/api/onboarding.js +4 -1
- package/dist/cloud/api/provider-env.d.ts +5 -0
- package/dist/cloud/api/provider-env.js +27 -0
- package/dist/cloud/api/providers.js +2 -0
- package/dist/cloud/api/test-helpers.js +130 -0
- package/dist/cloud/api/workspaces.js +38 -3
- package/dist/cloud/db/bulk-ingest.d.ts +88 -0
- package/dist/cloud/db/bulk-ingest.js +268 -0
- package/dist/cloud/db/drizzle.d.ts +33 -0
- package/dist/cloud/db/drizzle.js +174 -2
- package/dist/cloud/db/index.d.ts +24 -5
- package/dist/cloud/db/index.js +19 -4
- package/dist/cloud/db/schema.d.ts +397 -3
- package/dist/cloud/db/schema.js +75 -1
- package/dist/cloud/provisioner/index.d.ts +8 -0
- package/dist/cloud/provisioner/index.js +256 -50
- package/dist/cloud/server.js +47 -3
- package/dist/cloud/services/index.d.ts +1 -0
- package/dist/cloud/services/index.js +2 -0
- package/dist/cloud/services/nango.d.ts +3 -4
- package/dist/cloud/services/nango.js +11 -33
- package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
- package/dist/cloud/services/workspace-keepalive.js +234 -0
- package/dist/config/relay-config.d.ts +23 -0
- package/dist/config/relay-config.js +23 -0
- package/dist/daemon/agent-manager.d.ts +20 -1
- package/dist/daemon/agent-manager.js +51 -0
- package/dist/daemon/agent-registry.js +4 -4
- package/dist/daemon/agent-signing.d.ts +158 -0
- package/dist/daemon/agent-signing.js +523 -0
- package/dist/daemon/api.js +18 -1
- package/dist/daemon/cli-auth.d.ts +4 -1
- package/dist/daemon/cli-auth.js +55 -11
- package/dist/daemon/cloud-sync.d.ts +47 -1
- package/dist/daemon/cloud-sync.js +152 -3
- package/dist/daemon/connection.d.ts +28 -0
- package/dist/daemon/connection.js +113 -22
- package/dist/daemon/consensus-integration.d.ts +167 -0
- package/dist/daemon/consensus-integration.js +371 -0
- package/dist/daemon/consensus.d.ts +271 -0
- package/dist/daemon/consensus.js +632 -0
- package/dist/daemon/delivery-tracker.d.ts +34 -0
- package/dist/daemon/delivery-tracker.js +104 -0
- package/dist/daemon/enhanced-features.d.ts +118 -0
- package/dist/daemon/enhanced-features.js +178 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.js +5 -0
- package/dist/daemon/rate-limiter.d.ts +68 -0
- package/dist/daemon/rate-limiter.js +130 -0
- package/dist/daemon/router.d.ts +18 -11
- package/dist/daemon/router.js +57 -113
- package/dist/daemon/server.d.ts +13 -1
- package/dist/daemon/server.js +71 -9
- package/dist/daemon/sync-queue.d.ts +116 -0
- package/dist/daemon/sync-queue.js +361 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
- package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -0
- package/dist/dashboard/out/cloud/link.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -3
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +1 -1
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +1 -1
- package/dist/dashboard-server/server.js +244 -28
- package/dist/health-worker-manager.d.ts +62 -0
- package/dist/health-worker-manager.js +144 -0
- package/dist/health-worker.d.ts +9 -0
- package/dist/health-worker.js +79 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/memory/context-compaction.d.ts +156 -0
- package/dist/memory/context-compaction.js +453 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/protocol/channels.js +4 -4
- package/dist/protocol/framing.d.ts +72 -10
- package/dist/protocol/framing.js +194 -25
- package/dist/storage/adapter.d.ts +8 -1
- package/dist/storage/adapter.js +11 -0
- package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
- package/dist/storage/batched-sqlite-adapter.js +183 -0
- package/dist/storage/dead-letter-queue.d.ts +196 -0
- package/dist/storage/dead-letter-queue.js +427 -0
- package/dist/storage/dlq-adapter.d.ts +195 -0
- package/dist/storage/dlq-adapter.js +664 -0
- package/dist/trajectory/config.d.ts +32 -14
- package/dist/trajectory/config.js +38 -16
- package/dist/trajectory/integration.js +217 -64
- package/dist/utils/git-remote.d.ts +47 -0
- package/dist/utils/git-remote.js +125 -0
- package/dist/utils/id-generator.d.ts +35 -0
- package/dist/utils/id-generator.js +60 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/precompiled-patterns.d.ts +110 -0
- package/dist/utils/precompiled-patterns.js +322 -0
- package/dist/wrapper/auth-detection.js +1 -1
- package/dist/wrapper/base-wrapper.d.ts +40 -0
- package/dist/wrapper/base-wrapper.js +60 -6
- package/dist/wrapper/client.d.ts +14 -4
- package/dist/wrapper/client.js +89 -31
- package/dist/wrapper/idle-detector.d.ts +102 -0
- package/dist/wrapper/idle-detector.js +279 -0
- package/dist/wrapper/parser.d.ts +4 -0
- package/dist/wrapper/parser.js +19 -1
- package/dist/wrapper/pty-wrapper.d.ts +14 -2
- package/dist/wrapper/pty-wrapper.js +132 -32
- package/dist/wrapper/shared.d.ts +1 -1
- package/dist/wrapper/shared.js +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +20 -2
- package/dist/wrapper/tmux-wrapper.js +163 -40
- package/package.json +3 -1
- package/scripts/run-migrations.js +43 -0
- package/scripts/verify-schema.js +134 -0
- package/tests/benchmarks/protocol.bench.ts +310 -0
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
- package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
|
@@ -19,6 +19,7 @@ import { loadTeamsConfig } from '../bridge/teams-config.js';
|
|
|
19
19
|
import { getMemoryMonitor } from '../resiliency/memory-monitor.js';
|
|
20
20
|
import { detectWorkspacePath } from '../utils/project-namespace.js';
|
|
21
21
|
import { startCLIAuth, getAuthSession, cancelAuthSession, submitAuthCode, completeAuthSession, getSupportedProviders, } from '../daemon/cli-auth.js';
|
|
22
|
+
import { HealthWorkerManager, getHealthPort } from '../health-worker-manager.js';
|
|
22
23
|
/**
|
|
23
24
|
* Initialize cloud persistence for session tracking.
|
|
24
25
|
*
|
|
@@ -597,7 +598,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
597
598
|
return connectionPromise;
|
|
598
599
|
};
|
|
599
600
|
// Start default relay client connection (non-blocking)
|
|
600
|
-
|
|
601
|
+
// Use '_DashboardUI' to avoid conflicts with agents named 'Dashboard'
|
|
602
|
+
getRelayClient('_DashboardUI').catch(() => { });
|
|
601
603
|
// User bridge for human-to-human and human-to-agent messaging
|
|
602
604
|
const userBridge = new UserBridge({
|
|
603
605
|
socketPath,
|
|
@@ -693,6 +695,12 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
693
695
|
return false;
|
|
694
696
|
}
|
|
695
697
|
};
|
|
698
|
+
const isUserOnline = (username) => {
|
|
699
|
+
if (username === '*')
|
|
700
|
+
return true;
|
|
701
|
+
return onlineUsers.has(username) || userBridge.isUserRegistered(username);
|
|
702
|
+
};
|
|
703
|
+
const isRecipientOnline = (name) => (isAgentOnline(name) || isUserOnline(name));
|
|
696
704
|
// Helper to get team members from teams.json, agents.json, and spawner's active workers
|
|
697
705
|
const getTeamMembers = (teamName) => {
|
|
698
706
|
const members = new Set();
|
|
@@ -752,13 +760,15 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
752
760
|
}
|
|
753
761
|
else {
|
|
754
762
|
// Fail fast if target agent is offline (except broadcasts)
|
|
755
|
-
if (to !== '*' && !
|
|
756
|
-
return res.status(404).json({ error: `
|
|
763
|
+
if (to !== '*' && !isRecipientOnline(to)) {
|
|
764
|
+
return res.status(404).json({ error: `Recipient "${to}" is not online` });
|
|
757
765
|
}
|
|
758
766
|
targets = [to];
|
|
759
767
|
}
|
|
760
|
-
//
|
|
761
|
-
|
|
768
|
+
// Always use '_DashboardUI' client to avoid name conflicts with user agents
|
|
769
|
+
// (underscore prefix indicates system client, prevents collision if user names an agent "Dashboard")
|
|
770
|
+
// The sender name is preserved in message history/logs but not used for the relay connection
|
|
771
|
+
const relayClient = await getRelayClient('_DashboardUI');
|
|
762
772
|
if (!relayClient || relayClient.state !== 'READY') {
|
|
763
773
|
return res.status(503).json({ error: 'Relay daemon not connected' });
|
|
764
774
|
}
|
|
@@ -1103,6 +1113,25 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1103
1113
|
team: a.team,
|
|
1104
1114
|
});
|
|
1105
1115
|
});
|
|
1116
|
+
// Inject online human users (connected via dashboard WebSocket) into agentsMap
|
|
1117
|
+
// These users are tracked in onlineUsers for presence but need to appear in the agents list
|
|
1118
|
+
// with cli: 'dashboard' so they show up in the sidebar for DM conversations
|
|
1119
|
+
for (const [username, state] of onlineUsers) {
|
|
1120
|
+
// Don't overwrite existing entries (e.g., if user is also in team.json)
|
|
1121
|
+
if (!agentsMap.has(username)) {
|
|
1122
|
+
agentsMap.set(username, {
|
|
1123
|
+
name: username,
|
|
1124
|
+
role: 'User',
|
|
1125
|
+
cli: 'dashboard',
|
|
1126
|
+
messageCount: 0,
|
|
1127
|
+
status: 'online',
|
|
1128
|
+
lastSeen: state.info.lastSeen,
|
|
1129
|
+
lastActive: state.info.lastSeen,
|
|
1130
|
+
needsAttention: false,
|
|
1131
|
+
avatarUrl: state.info.avatarUrl,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1106
1135
|
// Update inbox counts if fallback mode; if storage, count messages addressed to agent
|
|
1107
1136
|
if (storage) {
|
|
1108
1137
|
for (const msg of allMessages) {
|
|
@@ -2067,16 +2096,49 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2067
2096
|
websocketClients: wss.clients.size,
|
|
2068
2097
|
});
|
|
2069
2098
|
});
|
|
2099
|
+
/**
|
|
2100
|
+
* GET /keep-alive - Keep-alive endpoint for Fly.io idle prevention
|
|
2101
|
+
* Called by cloud server when workspace has active agents running.
|
|
2102
|
+
* This inbound request counts as activity for Fly.io's request-based
|
|
2103
|
+
* concurrency tracking, preventing the machine from being idled.
|
|
2104
|
+
*/
|
|
2105
|
+
app.get('/keep-alive', (req, res) => {
|
|
2106
|
+
// Count online agents (seen within last 30 seconds)
|
|
2107
|
+
let activeAgents = 0;
|
|
2108
|
+
const agentsPath = path.join(teamDir, 'agents.json');
|
|
2109
|
+
if (fs.existsSync(agentsPath)) {
|
|
2110
|
+
try {
|
|
2111
|
+
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
2112
|
+
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
2113
|
+
activeAgents = (data.agents || []).filter((a) => {
|
|
2114
|
+
if (!a.lastSeen)
|
|
2115
|
+
return false;
|
|
2116
|
+
return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
|
|
2117
|
+
}).length;
|
|
2118
|
+
}
|
|
2119
|
+
catch {
|
|
2120
|
+
// Ignore parse errors
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
res.json({
|
|
2124
|
+
ok: true,
|
|
2125
|
+
activeAgents,
|
|
2126
|
+
timestamp: Date.now(),
|
|
2127
|
+
});
|
|
2128
|
+
});
|
|
2070
2129
|
// ===== CLI Auth API (for workspace-based provider authentication) =====
|
|
2071
2130
|
/**
|
|
2072
2131
|
* POST /auth/cli/:provider/start - Start CLI auth flow
|
|
2073
|
-
* Body: { useDeviceFlow?: boolean }
|
|
2132
|
+
* Body: { useDeviceFlow?: boolean, userId?: string }
|
|
2133
|
+
*
|
|
2134
|
+
* When userId is provided, credentials are stored per-user at /data/users/{userId}/.{provider}/
|
|
2135
|
+
* This allows multiple users to share a workspace with their own CLI credentials.
|
|
2074
2136
|
*/
|
|
2075
2137
|
app.post('/auth/cli/:provider/start', async (req, res) => {
|
|
2076
2138
|
const { provider } = req.params;
|
|
2077
|
-
const { useDeviceFlow } = req.body || {};
|
|
2139
|
+
const { useDeviceFlow, userId } = req.body || {};
|
|
2078
2140
|
try {
|
|
2079
|
-
const session = await startCLIAuth(provider, { useDeviceFlow });
|
|
2141
|
+
const session = await startCLIAuth(provider, { useDeviceFlow, userId });
|
|
2080
2142
|
res.json({
|
|
2081
2143
|
sessionId: session.id,
|
|
2082
2144
|
status: session.status,
|
|
@@ -2239,18 +2301,35 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2239
2301
|
*/
|
|
2240
2302
|
app.get('/auth/cli/openai/check', async (req, res) => {
|
|
2241
2303
|
try {
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2244
|
-
const
|
|
2304
|
+
// Get userId from query params for per-user credential checking
|
|
2305
|
+
// Multiple users can share a workspace, each with their own CLI credentials
|
|
2306
|
+
const userId = req.query.userId;
|
|
2307
|
+
let credPath;
|
|
2308
|
+
if (userId) {
|
|
2309
|
+
// Per-user credential path: /data/users/{userId}/.codex/auth.json
|
|
2310
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR || '/data';
|
|
2311
|
+
credPath = path.join(dataDir, 'users', userId, '.codex', 'auth.json');
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
// Fallback to workspace-wide path for backwards compatibility
|
|
2315
|
+
const homedir = process.env.HOME || '/home/workspace';
|
|
2316
|
+
credPath = path.join(homedir, '.codex', 'auth.json');
|
|
2317
|
+
}
|
|
2245
2318
|
if (!fs.existsSync(credPath)) {
|
|
2246
2319
|
return res.json({ authenticated: false });
|
|
2247
2320
|
}
|
|
2248
2321
|
const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
2249
2322
|
// Check if we have a valid access token or API key
|
|
2250
|
-
|
|
2323
|
+
// Codex stores tokens in a nested 'tokens' object: { tokens: { access_token, refresh_token } }
|
|
2324
|
+
const hasToken = !!(creds.access_token ||
|
|
2325
|
+
creds.token ||
|
|
2326
|
+
creds.api_key ||
|
|
2327
|
+
creds.OPENAI_API_KEY ||
|
|
2328
|
+
creds.tokens?.access_token ||
|
|
2329
|
+
creds.tokens?.refresh_token);
|
|
2251
2330
|
res.json({ authenticated: hasToken });
|
|
2252
2331
|
}
|
|
2253
|
-
catch (
|
|
2332
|
+
catch (_error) {
|
|
2254
2333
|
// File doesn't exist or is invalid
|
|
2255
2334
|
res.json({ authenticated: false });
|
|
2256
2335
|
}
|
|
@@ -2858,10 +2937,20 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2858
2937
|
res.status(500).json({ error: 'Failed to list agents' });
|
|
2859
2938
|
}
|
|
2860
2939
|
});
|
|
2940
|
+
// ===== Agent Status API =====
|
|
2941
|
+
/**
|
|
2942
|
+
* GET /api/agents/:name/online - Check if an agent is online
|
|
2943
|
+
* Used by wrappers to wait for spawned agents before sending tasks.
|
|
2944
|
+
*/
|
|
2945
|
+
app.get('/api/agents/:name/online', (req, res) => {
|
|
2946
|
+
const { name } = req.params;
|
|
2947
|
+
const online = isAgentOnline(name);
|
|
2948
|
+
res.json({ name, online });
|
|
2949
|
+
});
|
|
2861
2950
|
// ===== Agent Spawn API =====
|
|
2862
2951
|
/**
|
|
2863
2952
|
* POST /api/spawn - Spawn a new agent
|
|
2864
|
-
* Body: { name: string, cli?: string, task?: string, team?: string, shadowMode?, shadowAgent?, shadowOf?, shadowTriggers?, shadowSpeakOn? }
|
|
2953
|
+
* Body: { name: string, cli?: string, task?: string, team?: string, spawnerName?, cwd?, interactive?, shadowMode?, shadowAgent?, shadowOf?, shadowTriggers?, shadowSpeakOn? }
|
|
2865
2954
|
*/
|
|
2866
2955
|
app.post('/api/spawn', async (req, res) => {
|
|
2867
2956
|
if (!spawner) {
|
|
@@ -2870,7 +2959,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2870
2959
|
error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
|
|
2871
2960
|
});
|
|
2872
2961
|
}
|
|
2873
|
-
const { name, cli = 'claude', task = '', team, interactive, shadowMode, shadowAgent, shadowOf, shadowTriggers, shadowSpeakOn, } = req.body;
|
|
2962
|
+
const { name, cli = 'claude', task = '', team, spawnerName, cwd, interactive, shadowMode, shadowAgent, shadowOf, shadowTriggers, shadowSpeakOn, userId, } = req.body;
|
|
2874
2963
|
if (!name || typeof name !== 'string') {
|
|
2875
2964
|
return res.status(400).json({
|
|
2876
2965
|
success: false,
|
|
@@ -2883,12 +2972,15 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2883
2972
|
cli,
|
|
2884
2973
|
task,
|
|
2885
2974
|
team: team || undefined, // Optional team name
|
|
2975
|
+
spawnerName: spawnerName || undefined, // For policy enforcement
|
|
2976
|
+
cwd: cwd || undefined, // Working directory
|
|
2886
2977
|
interactive, // Disables auto-accept for auth setup flows
|
|
2887
2978
|
shadowMode,
|
|
2888
2979
|
shadowAgent,
|
|
2889
2980
|
shadowOf,
|
|
2890
2981
|
shadowTriggers,
|
|
2891
2982
|
shadowSpeakOn,
|
|
2983
|
+
userId: typeof userId === 'string' ? userId : undefined,
|
|
2892
2984
|
};
|
|
2893
2985
|
const result = await spawner.spawn(request);
|
|
2894
2986
|
if (result.success) {
|
|
@@ -2997,19 +3089,70 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
2997
3089
|
});
|
|
2998
3090
|
/**
|
|
2999
3091
|
* GET /api/spawned - List active spawned agents
|
|
3092
|
+
*
|
|
3093
|
+
* Returns agents from two sources:
|
|
3094
|
+
* 1. Spawner's active workers (in-memory tracking)
|
|
3095
|
+
* 2. Daemon's agents.json registry (persisted, survives restarts)
|
|
3096
|
+
*
|
|
3097
|
+
* This fallback ensures docker deployments show agents even after
|
|
3098
|
+
* container restarts when spawner's in-memory state is lost but
|
|
3099
|
+
* agents have reconnected to the daemon.
|
|
3000
3100
|
*/
|
|
3001
3101
|
app.get('/api/spawned', (req, res) => {
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3102
|
+
// Collect agents from all available sources
|
|
3103
|
+
const agentsByName = new Map();
|
|
3104
|
+
// Source 1: Spawner's active workers (authoritative for spawned agents)
|
|
3105
|
+
if (spawner) {
|
|
3106
|
+
for (const worker of spawner.getActiveWorkers()) {
|
|
3107
|
+
agentsByName.set(worker.name, {
|
|
3108
|
+
name: worker.name,
|
|
3109
|
+
cli: worker.cli,
|
|
3110
|
+
pid: worker.pid,
|
|
3111
|
+
spawnedAt: worker.spawnedAt,
|
|
3112
|
+
task: worker.task,
|
|
3113
|
+
team: worker.team,
|
|
3114
|
+
source: 'spawner',
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3008
3117
|
}
|
|
3009
|
-
|
|
3118
|
+
// Source 2: Daemon's agents.json registry (fallback for docker restarts)
|
|
3119
|
+
// Only include agents not already tracked by spawner
|
|
3120
|
+
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3121
|
+
if (fs.existsSync(agentsPath)) {
|
|
3122
|
+
try {
|
|
3123
|
+
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3124
|
+
const registeredAgents = data.agents || [];
|
|
3125
|
+
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
3126
|
+
for (const agent of registeredAgents) {
|
|
3127
|
+
// Skip if already tracked by spawner
|
|
3128
|
+
if (agentsByName.has(agent.name))
|
|
3129
|
+
continue;
|
|
3130
|
+
// Only include recently active agents (within 30s heartbeat window)
|
|
3131
|
+
const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
|
|
3132
|
+
if (lastSeen < thirtySecondsAgo)
|
|
3133
|
+
continue;
|
|
3134
|
+
agentsByName.set(agent.name, {
|
|
3135
|
+
name: agent.name,
|
|
3136
|
+
cli: agent.cli || 'unknown',
|
|
3137
|
+
spawnedAt: agent.connectedAt ? new Date(agent.connectedAt).getTime() : undefined,
|
|
3138
|
+
team: agent.team,
|
|
3139
|
+
source: 'daemon',
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
catch (err) {
|
|
3144
|
+
console.error('[api/spawned] Failed to read agents.json:', err);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const agents = Array.from(agentsByName.values());
|
|
3010
3148
|
res.json({
|
|
3011
3149
|
success: true,
|
|
3012
3150
|
agents,
|
|
3151
|
+
// Include source info for debugging
|
|
3152
|
+
sources: {
|
|
3153
|
+
spawnerEnabled: !!spawner,
|
|
3154
|
+
daemonAgentsFile: fs.existsSync(agentsPath),
|
|
3155
|
+
},
|
|
3013
3156
|
});
|
|
3014
3157
|
});
|
|
3015
3158
|
/**
|
|
@@ -3043,6 +3186,53 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3043
3186
|
});
|
|
3044
3187
|
}
|
|
3045
3188
|
});
|
|
3189
|
+
/**
|
|
3190
|
+
* POST /api/agents/by-name/:name/interrupt - Send ESC sequence to interrupt an agent
|
|
3191
|
+
*
|
|
3192
|
+
* Sends ESC ESC (0x1b 0x1b) to the agent's PTY to interrupt the current operation.
|
|
3193
|
+
* This is useful for breaking agents out of stuck loops without terminating them.
|
|
3194
|
+
*/
|
|
3195
|
+
app.post('/api/agents/by-name/:name/interrupt', (req, res) => {
|
|
3196
|
+
if (!spawner) {
|
|
3197
|
+
return res.status(503).json({
|
|
3198
|
+
success: false,
|
|
3199
|
+
error: 'Spawner not enabled',
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
const { name } = req.params;
|
|
3203
|
+
// Check if agent exists
|
|
3204
|
+
if (!spawner.hasWorker(name)) {
|
|
3205
|
+
return res.status(404).json({
|
|
3206
|
+
success: false,
|
|
3207
|
+
error: `Agent ${name} not found or not spawned`,
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
try {
|
|
3211
|
+
// Send ESC ESC sequence to interrupt the agent
|
|
3212
|
+
// ESC = 0x1b in hexadecimal
|
|
3213
|
+
const success = spawner.sendWorkerInput(name, '\x1b\x1b');
|
|
3214
|
+
if (success) {
|
|
3215
|
+
console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`);
|
|
3216
|
+
res.json({
|
|
3217
|
+
success: true,
|
|
3218
|
+
message: `Interrupt signal sent to ${name}`,
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
else {
|
|
3222
|
+
res.status(500).json({
|
|
3223
|
+
success: false,
|
|
3224
|
+
error: `Failed to send interrupt to ${name}`,
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
catch (err) {
|
|
3229
|
+
console.error('[api] Interrupt error:', err);
|
|
3230
|
+
res.status(500).json({
|
|
3231
|
+
success: false,
|
|
3232
|
+
error: err.message,
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3046
3236
|
/**
|
|
3047
3237
|
* GET /api/trajectory - Get current trajectory status
|
|
3048
3238
|
*/
|
|
@@ -3311,7 +3501,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3311
3501
|
}
|
|
3312
3502
|
// Try to send message to agent
|
|
3313
3503
|
try {
|
|
3314
|
-
const client = await getRelayClient('
|
|
3504
|
+
const client = await getRelayClient('_DashboardUI');
|
|
3315
3505
|
if (client) {
|
|
3316
3506
|
await client.sendMessage(agentName, responseMessage, 'message');
|
|
3317
3507
|
}
|
|
@@ -3341,7 +3531,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3341
3531
|
responseMessage += `\nReason: ${reason}`;
|
|
3342
3532
|
}
|
|
3343
3533
|
try {
|
|
3344
|
-
const client = await getRelayClient('
|
|
3534
|
+
const client = await getRelayClient('_DashboardUI');
|
|
3345
3535
|
if (client) {
|
|
3346
3536
|
await client.sendMessage(agentName, responseMessage, 'message');
|
|
3347
3537
|
}
|
|
@@ -3529,7 +3719,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3529
3719
|
tasks.set(task.id, task);
|
|
3530
3720
|
// Send task to agent via relay
|
|
3531
3721
|
try {
|
|
3532
|
-
const client = await getRelayClient('
|
|
3722
|
+
const client = await getRelayClient('_DashboardUI');
|
|
3533
3723
|
if (client) {
|
|
3534
3724
|
const taskMessage = `TASK ASSIGNED [${priority.toUpperCase()}]: ${title}\n\n${description || 'No additional details.'}`;
|
|
3535
3725
|
await client.sendMessage(agentName, taskMessage, 'message');
|
|
@@ -3577,7 +3767,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3577
3767
|
// Notify agent of cancellation if task is still active
|
|
3578
3768
|
if (task.status === 'pending' || task.status === 'assigned' || task.status === 'in_progress') {
|
|
3579
3769
|
try {
|
|
3580
|
-
const client = await getRelayClient('
|
|
3770
|
+
const client = await getRelayClient('_DashboardUI');
|
|
3581
3771
|
if (client) {
|
|
3582
3772
|
await client.sendMessage(task.agentName, `TASK CANCELLED: ${task.title}`, 'message');
|
|
3583
3773
|
}
|
|
@@ -3651,7 +3841,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3651
3841
|
return res.status(400).json({ success: false, error: 'Message content is required' });
|
|
3652
3842
|
}
|
|
3653
3843
|
try {
|
|
3654
|
-
const client = await getRelayClient('
|
|
3844
|
+
const client = await getRelayClient('_DashboardUI');
|
|
3655
3845
|
if (!client) {
|
|
3656
3846
|
return res.status(503).json({
|
|
3657
3847
|
success: false,
|
|
@@ -3758,13 +3948,39 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
3758
3948
|
console.log(`Requested dashboard port ${port} is busy; switching to ${availablePort}.`);
|
|
3759
3949
|
}
|
|
3760
3950
|
return new Promise((resolve, reject) => {
|
|
3761
|
-
server.listen(availablePort, () => {
|
|
3951
|
+
server.listen(availablePort, async () => {
|
|
3762
3952
|
console.log(`Dashboard running at http://localhost:${availablePort}`);
|
|
3763
3953
|
console.log(`Monitoring: ${dataDir}`);
|
|
3764
3954
|
// Set the dashboard port on spawner so spawned agents can use the API for nested spawns
|
|
3765
3955
|
if (spawner) {
|
|
3766
3956
|
spawner.setDashboardPort(availablePort);
|
|
3767
3957
|
}
|
|
3958
|
+
// Start health worker on separate thread for reliable health checks
|
|
3959
|
+
// This ensures health checks respond even when main event loop is blocked
|
|
3960
|
+
const healthPort = getHealthPort(availablePort);
|
|
3961
|
+
const healthWorker = new HealthWorkerManager({ port: healthPort }, {
|
|
3962
|
+
getUptime: () => process.uptime(),
|
|
3963
|
+
getMemoryMB: () => Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
3964
|
+
getRelayConnected: () => {
|
|
3965
|
+
const defaultClient = relayClients.get('Dashboard');
|
|
3966
|
+
return defaultClient?.state === 'READY';
|
|
3967
|
+
},
|
|
3968
|
+
getAgentCount: () => relayClients.size,
|
|
3969
|
+
getStatus: () => {
|
|
3970
|
+
const socketExists = fs.existsSync(socketPath);
|
|
3971
|
+
if (!socketExists)
|
|
3972
|
+
return 'degraded';
|
|
3973
|
+
const defaultClient = relayClients.get('Dashboard');
|
|
3974
|
+
return defaultClient?.state === 'READY' ? 'healthy' : 'busy';
|
|
3975
|
+
},
|
|
3976
|
+
});
|
|
3977
|
+
try {
|
|
3978
|
+
await healthWorker.start();
|
|
3979
|
+
console.log(`Health check worker running at http://localhost:${healthPort}/health`);
|
|
3980
|
+
}
|
|
3981
|
+
catch (err) {
|
|
3982
|
+
console.warn('[dashboard] Failed to start health worker, using main thread health check:', err);
|
|
3983
|
+
}
|
|
3768
3984
|
resolve(availablePort);
|
|
3769
3985
|
});
|
|
3770
3986
|
server.on('error', (err) => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Worker Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the health check worker thread, sending periodic stats updates
|
|
5
|
+
* and handling worker lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
export interface HealthWorkerConfig {
|
|
8
|
+
/** Port for health check server (default: main port + 1) */
|
|
9
|
+
port: number;
|
|
10
|
+
/** Interval for sending stats updates (default: 5000ms) */
|
|
11
|
+
statsInterval?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface HealthStatsProvider {
|
|
14
|
+
getUptime: () => number;
|
|
15
|
+
getMemoryMB: () => number;
|
|
16
|
+
getRelayConnected: () => boolean;
|
|
17
|
+
getAgentCount: () => number;
|
|
18
|
+
getStatus: () => 'healthy' | 'busy' | 'degraded';
|
|
19
|
+
}
|
|
20
|
+
export declare class HealthWorkerManager {
|
|
21
|
+
private worker;
|
|
22
|
+
private statsInterval;
|
|
23
|
+
private config;
|
|
24
|
+
private statsProvider;
|
|
25
|
+
private ready;
|
|
26
|
+
constructor(config: HealthWorkerConfig, statsProvider: HealthStatsProvider);
|
|
27
|
+
/**
|
|
28
|
+
* Start the health worker thread
|
|
29
|
+
*/
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Stop the health worker thread
|
|
33
|
+
*/
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Check if worker is ready
|
|
37
|
+
*/
|
|
38
|
+
isReady(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Get the port the health worker is listening on
|
|
41
|
+
*/
|
|
42
|
+
getPort(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Start periodic stats updates to worker
|
|
45
|
+
*/
|
|
46
|
+
private startStatsUpdates;
|
|
47
|
+
/**
|
|
48
|
+
* Stop stats updates
|
|
49
|
+
*/
|
|
50
|
+
private stopStatsUpdates;
|
|
51
|
+
/**
|
|
52
|
+
* Send current stats to worker
|
|
53
|
+
*/
|
|
54
|
+
private sendStats;
|
|
55
|
+
}
|
|
56
|
+
/** Default health port offset from main port */
|
|
57
|
+
export declare const HEALTH_PORT_OFFSET = 1;
|
|
58
|
+
/**
|
|
59
|
+
* Calculate health port from main port
|
|
60
|
+
*/
|
|
61
|
+
export declare function getHealthPort(mainPort: number): number;
|
|
62
|
+
//# sourceMappingURL=health-worker-manager.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Worker Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the health check worker thread, sending periodic stats updates
|
|
5
|
+
* and handling worker lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
import { Worker } from 'node:worker_threads';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
export class HealthWorkerManager {
|
|
13
|
+
worker = null;
|
|
14
|
+
statsInterval = null;
|
|
15
|
+
config;
|
|
16
|
+
statsProvider;
|
|
17
|
+
ready = false;
|
|
18
|
+
constructor(config, statsProvider) {
|
|
19
|
+
this.config = {
|
|
20
|
+
statsInterval: 5000,
|
|
21
|
+
...config,
|
|
22
|
+
};
|
|
23
|
+
this.statsProvider = statsProvider;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the health worker thread
|
|
27
|
+
*/
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.worker) {
|
|
30
|
+
console.warn('[health-manager] Worker already running');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
// Worker script path - handle both dev (src) and prod (dist)
|
|
35
|
+
const workerPath = path.join(__dirname, 'health-worker.js');
|
|
36
|
+
this.worker = new Worker(workerPath, {
|
|
37
|
+
workerData: { port: this.config.port },
|
|
38
|
+
});
|
|
39
|
+
this.worker.on('message', (msg) => {
|
|
40
|
+
if (msg.type === 'ready') {
|
|
41
|
+
this.ready = true;
|
|
42
|
+
console.log(`[health-manager] Worker ready on port ${msg.port}`);
|
|
43
|
+
this.startStatsUpdates();
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
else if (msg.type === 'error') {
|
|
47
|
+
console.error('[health-manager] Worker error:', msg.error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.worker.on('error', (err) => {
|
|
51
|
+
console.error('[health-manager] Worker thread error:', err);
|
|
52
|
+
if (!this.ready) {
|
|
53
|
+
reject(err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
this.worker.on('exit', (code) => {
|
|
57
|
+
console.log(`[health-manager] Worker exited with code ${code}`);
|
|
58
|
+
this.ready = false;
|
|
59
|
+
this.worker = null;
|
|
60
|
+
this.stopStatsUpdates();
|
|
61
|
+
});
|
|
62
|
+
// Timeout for worker startup
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
if (!this.ready) {
|
|
65
|
+
reject(new Error('Health worker startup timeout'));
|
|
66
|
+
}
|
|
67
|
+
}, 10000);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Stop the health worker thread
|
|
72
|
+
*/
|
|
73
|
+
async stop() {
|
|
74
|
+
this.stopStatsUpdates();
|
|
75
|
+
if (this.worker) {
|
|
76
|
+
await this.worker.terminate();
|
|
77
|
+
this.worker = null;
|
|
78
|
+
this.ready = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if worker is ready
|
|
83
|
+
*/
|
|
84
|
+
isReady() {
|
|
85
|
+
return this.ready;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get the port the health worker is listening on
|
|
89
|
+
*/
|
|
90
|
+
getPort() {
|
|
91
|
+
return this.config.port;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Start periodic stats updates to worker
|
|
95
|
+
*/
|
|
96
|
+
startStatsUpdates() {
|
|
97
|
+
if (this.statsInterval)
|
|
98
|
+
return;
|
|
99
|
+
// Send initial stats
|
|
100
|
+
this.sendStats();
|
|
101
|
+
// Send periodic updates
|
|
102
|
+
this.statsInterval = setInterval(() => {
|
|
103
|
+
this.sendStats();
|
|
104
|
+
}, this.config.statsInterval);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Stop stats updates
|
|
108
|
+
*/
|
|
109
|
+
stopStatsUpdates() {
|
|
110
|
+
if (this.statsInterval) {
|
|
111
|
+
clearInterval(this.statsInterval);
|
|
112
|
+
this.statsInterval = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Send current stats to worker
|
|
117
|
+
*/
|
|
118
|
+
sendStats() {
|
|
119
|
+
if (!this.worker || !this.ready)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
const stats = {
|
|
123
|
+
uptime: this.statsProvider.getUptime(),
|
|
124
|
+
memoryMB: this.statsProvider.getMemoryMB(),
|
|
125
|
+
relayConnected: this.statsProvider.getRelayConnected(),
|
|
126
|
+
agentCount: this.statsProvider.getAgentCount(),
|
|
127
|
+
status: this.statsProvider.getStatus(),
|
|
128
|
+
};
|
|
129
|
+
this.worker.postMessage(stats);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error('[health-manager] Failed to send stats:', err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Default health port offset from main port */
|
|
137
|
+
export const HEALTH_PORT_OFFSET = 1;
|
|
138
|
+
/**
|
|
139
|
+
* Calculate health port from main port
|
|
140
|
+
*/
|
|
141
|
+
export function getHealthPort(mainPort) {
|
|
142
|
+
return mainPort + HEALTH_PORT_OFFSET;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=health-worker-manager.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Worker Thread
|
|
3
|
+
*
|
|
4
|
+
* Runs a minimal HTTP server on a separate thread to handle health checks.
|
|
5
|
+
* This ensures health checks respond even when the main event loop is blocked
|
|
6
|
+
* by heavy compute tasks (builds, large file operations, etc.).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=health-worker.d.ts.map
|