@agent-relay/dashboard-server 2.0.82 → 2.0.83-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +242 -127
- package/dist/server.js.map +1 -1
- package/dist/services/broker-spawn-reader.d.ts +40 -0
- package/dist/services/broker-spawn-reader.d.ts.map +1 -0
- package/dist/services/broker-spawn-reader.js +154 -0
- package/dist/services/broker-spawn-reader.js.map +1 -0
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.d.ts.map +1 -1
- package/out/404.html +1 -1
- package/out/about.html +1 -1
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +1 -1
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
- package/out/blog.html +1 -1
- package/out/blog.txt +1 -1
- package/out/careers.html +1 -1
- package/out/careers.txt +1 -1
- package/out/changelog.html +1 -1
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +1 -1
- package/out/complete-profile.html +1 -1
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +1 -1
- package/out/contact.txt +1 -1
- package/out/docs.html +1 -1
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +1 -1
- package/out/index.html +1 -1
- package/out/index.txt +1 -1
- package/out/login.html +1 -1
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +1 -1
- package/out/pricing.html +1 -1
- package/out/pricing.txt +1 -1
- package/out/privacy.html +1 -1
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +1 -1
- package/out/security.txt +1 -1
- package/out/signup.html +1 -1
- package/out/signup.txt +1 -1
- package/out/terms.html +1 -1
- package/out/terms.txt +1 -1
- package/package.json +1 -1
- /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → TnAI-TAQ4-bNJRL3Ln3NB}/_buildManifest.js +0 -0
- /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → TnAI-TAQ4-bNJRL3Ln3NB}/_ssgManifest.js +0 -0
package/dist/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { listTrajectorySteps, getTrajectoryStatus, getTrajectoryHistory } from '
|
|
|
18
18
|
import { loadTeamsConfig } from '@agent-relay/config';
|
|
19
19
|
import { getMemoryMonitor } from '@agent-relay/resiliency';
|
|
20
20
|
import { detectWorkspacePath, getAgentOutboxTemplate } from '@agent-relay/config';
|
|
21
|
+
import { BrokerSpawnReader } from './services/broker-spawn-reader.js';
|
|
21
22
|
// Dynamically find the dashboard static files directory
|
|
22
23
|
// We can't import from @agent-relay/dashboard directly due to circular dependency
|
|
23
24
|
function findDashboardDir() {
|
|
@@ -320,6 +321,24 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
320
321
|
? { port: portOrOptions, dataDir: dataDirArg, teamDir: teamDirArg, dbPath: dbPathArg }
|
|
321
322
|
: portOrOptions;
|
|
322
323
|
const { port, dataDir, teamDir, dbPath, enableSpawner, projectRoot, tmuxSession, onMarkSpawning, onClearSpawning, verbose, spawnManager: externalSpawnManager } = options;
|
|
324
|
+
let { relayAdapter } = options;
|
|
325
|
+
// Auto-create a RelayAdapter when projectRoot is available and no adapter/spawnManager is passed.
|
|
326
|
+
// This makes the dashboard self-contained — `relay-dashboard-server --integrated --project-root .`
|
|
327
|
+
// works without the CLI.
|
|
328
|
+
if (!relayAdapter && !externalSpawnManager && enableSpawner && projectRoot) {
|
|
329
|
+
try {
|
|
330
|
+
const brokerSdk = await import('@agent-relay/broker-sdk');
|
|
331
|
+
relayAdapter = new brokerSdk.RelayAdapter({
|
|
332
|
+
cwd: projectRoot,
|
|
333
|
+
clientName: 'dashboard',
|
|
334
|
+
});
|
|
335
|
+
console.log('[dashboard] Auto-created RelayAdapter for broker mode');
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// @agent-relay/broker-sdk not installed — fall back to legacy AgentSpawner
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const useBrokerAdapter = !!relayAdapter;
|
|
323
342
|
// Debug logging helper - only logs when verbose is true or VERBOSE env var is set
|
|
324
343
|
const isVerbose = verbose || process.env.VERBOSE === 'true';
|
|
325
344
|
const debug = (message) => {
|
|
@@ -487,31 +506,41 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
487
506
|
// tool directories like ~/.nvm contain package.json markers.
|
|
488
507
|
const workspacePath = projectRoot || detectWorkspacePath(dataDir);
|
|
489
508
|
console.log(`[dashboard] Workspace path: ${workspacePath}`);
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
509
|
+
// Spawner / SpawnReader setup — three modes:
|
|
510
|
+
// 1. Broker adapter mode: relayAdapter provides spawn/release/list via Rust binary stdio
|
|
511
|
+
// 2. External SpawnManager: daemon co-located, read ops from daemon's SpawnManager
|
|
512
|
+
// 3. Local AgentSpawner: standalone mode, spawns agents directly
|
|
494
513
|
const useExternalSpawnManager = !!externalSpawnManager;
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
let brokerSpawnReader;
|
|
515
|
+
let spawner;
|
|
516
|
+
if (useBrokerAdapter) {
|
|
517
|
+
// Mode 1: Broker adapter — create BrokerSpawnReader backed by RelayAdapter
|
|
518
|
+
brokerSpawnReader = new BrokerSpawnReader(relayAdapter);
|
|
519
|
+
await relayAdapter.start();
|
|
520
|
+
await brokerSpawnReader.initialize();
|
|
521
|
+
console.log('[dashboard] Using broker adapter for spawn operations');
|
|
522
|
+
}
|
|
523
|
+
else if (!useExternalSpawnManager && enableSpawner) {
|
|
524
|
+
// Mode 3: Local AgentSpawner (legacy standalone)
|
|
525
|
+
spawner = new AgentSpawner({
|
|
497
526
|
projectRoot: workspacePath,
|
|
498
527
|
tmuxSession,
|
|
499
528
|
dashboardPort: port,
|
|
500
529
|
onMarkSpawning,
|
|
501
530
|
onClearSpawning,
|
|
502
|
-
})
|
|
503
|
-
|
|
531
|
+
});
|
|
532
|
+
}
|
|
504
533
|
// spawnReader provides read-only access to spawner state.
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
534
|
+
// Priority: broker adapter > external SpawnManager > local AgentSpawner
|
|
535
|
+
const spawnReader = brokerSpawnReader || externalSpawnManager || spawner;
|
|
536
|
+
if (useBrokerAdapter) {
|
|
537
|
+
// Already logged above
|
|
538
|
+
}
|
|
539
|
+
else if (useExternalSpawnManager) {
|
|
509
540
|
console.log('[dashboard] Using daemon SpawnManager for spawn operations (SDK-routed)');
|
|
510
541
|
}
|
|
511
|
-
// Initialize cloud persistence
|
|
512
|
-
|
|
513
|
-
if (spawner && !useExternalSpawnManager) {
|
|
514
|
-
// Use workspace ID from env or generate from project root
|
|
542
|
+
// Initialize cloud persistence (only for legacy local AgentSpawner mode)
|
|
543
|
+
if (spawner && !useExternalSpawnManager && !useBrokerAdapter) {
|
|
515
544
|
const workspaceId = process.env.RELAY_WORKSPACE_ID ||
|
|
516
545
|
crypto.createHash('sha256').update(projectRoot || dataDir).digest('hex').slice(0, 36);
|
|
517
546
|
initCloudPersistence(workspaceId).then((cloudHandler) => {
|
|
@@ -522,17 +551,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
522
551
|
console.warn('[dashboard] Failed to initialize cloud persistence:', err);
|
|
523
552
|
});
|
|
524
553
|
}
|
|
525
|
-
// Initialize memory monitoring for cloud deployments
|
|
526
|
-
if (spawnReader && (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true')) {
|
|
554
|
+
// Initialize memory monitoring for cloud deployments (skip in broker mode — use adapter.getMetrics())
|
|
555
|
+
if (!useBrokerAdapter && spawnReader && (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true')) {
|
|
527
556
|
try {
|
|
528
557
|
const memoryMonitor = getMemoryMonitor({
|
|
529
|
-
checkIntervalMs: 10000,
|
|
558
|
+
checkIntervalMs: 10000,
|
|
530
559
|
enableTrendAnalysis: true,
|
|
531
560
|
enableProactiveAlerts: true,
|
|
532
561
|
});
|
|
533
562
|
memoryMonitor.start();
|
|
534
563
|
console.log('[dashboard] Memory monitoring enabled');
|
|
535
|
-
// Register existing workers with memory monitor
|
|
536
564
|
const workers = spawnReader.getActiveWorkers();
|
|
537
565
|
for (const worker of workers) {
|
|
538
566
|
if (worker.pid) {
|
|
@@ -882,13 +910,56 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
882
910
|
});
|
|
883
911
|
}
|
|
884
912
|
// Relay clients for sending messages from dashboard
|
|
885
|
-
//
|
|
913
|
+
// In broker adapter mode, messages go through the single RelayAdapter.
|
|
914
|
+
// In legacy mode, per-user RelayClient connections are created via socket.
|
|
886
915
|
const socketPath = path.join(dataDir, 'relay.sock');
|
|
887
916
|
const relayClients = new Map();
|
|
888
917
|
// Forward declaration - initialized later, used by getRelayClient to avoid duplicate connections
|
|
889
918
|
// eslint-disable-next-line prefer-const
|
|
890
919
|
let userBridge;
|
|
920
|
+
// In broker mode, create shim objects that satisfy the RelayClient interface
|
|
921
|
+
// used by the rest of the server (sendMessage, state, spawn, release).
|
|
922
|
+
// Each shim captures a sender name for proper message attribution.
|
|
923
|
+
const brokerClientShimCache = new Map();
|
|
924
|
+
const getBrokerClientShim = (senderName) => {
|
|
925
|
+
if (!useBrokerAdapter)
|
|
926
|
+
return undefined;
|
|
927
|
+
let shim = brokerClientShimCache.get(senderName);
|
|
928
|
+
if (!shim) {
|
|
929
|
+
shim = {
|
|
930
|
+
state: 'READY',
|
|
931
|
+
sendMessage: async (to, body, _kind, _data, thread) => {
|
|
932
|
+
await relayAdapter.sendMessage({ to, text: body, from: senderName, threadId: thread });
|
|
933
|
+
return true;
|
|
934
|
+
},
|
|
935
|
+
spawn: async (req) => {
|
|
936
|
+
return relayAdapter.spawn({
|
|
937
|
+
name: req.name,
|
|
938
|
+
cli: req.cli,
|
|
939
|
+
task: req.task,
|
|
940
|
+
team: req.team,
|
|
941
|
+
cwd: req.cwd,
|
|
942
|
+
interactive: req.interactive,
|
|
943
|
+
shadowMode: req.shadowMode,
|
|
944
|
+
shadowOf: req.shadowOf,
|
|
945
|
+
spawnerName: req.spawnerName,
|
|
946
|
+
userId: req.userId,
|
|
947
|
+
includeWorkflowConventions: req.includeWorkflowConventions,
|
|
948
|
+
});
|
|
949
|
+
},
|
|
950
|
+
release: async (name) => {
|
|
951
|
+
return relayAdapter.release(name);
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
brokerClientShimCache.set(senderName, shim);
|
|
955
|
+
}
|
|
956
|
+
return shim;
|
|
957
|
+
};
|
|
891
958
|
const notifyDaemonOfMembershipUpdate = async (channel, member, action, workspaceId) => {
|
|
959
|
+
if (useBrokerAdapter) {
|
|
960
|
+
// In broker mode, channel membership is managed differently — no _router message needed
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
892
963
|
const client = await getRelayClient('Dashboard');
|
|
893
964
|
if (!client || client.state !== 'READY') {
|
|
894
965
|
return;
|
|
@@ -905,14 +976,20 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
905
976
|
// Track pending client connections to prevent race conditions
|
|
906
977
|
const pendingConnections = new Map();
|
|
907
978
|
// Get or create a relay client for a specific sender
|
|
979
|
+
// In broker adapter mode, returns a shim backed by RelayAdapter.
|
|
980
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
908
981
|
const getRelayClient = async (senderName = 'Dashboard', entityType) => {
|
|
982
|
+
// Broker adapter mode — return per-sender shim (always "ready")
|
|
983
|
+
if (useBrokerAdapter) {
|
|
984
|
+
return getBrokerClientShim(senderName);
|
|
985
|
+
}
|
|
986
|
+
// --- Legacy socket-based mode below ---
|
|
909
987
|
// Check if we already have a connected client for this sender
|
|
910
988
|
const existing = relayClients.get(senderName);
|
|
911
989
|
if (existing && existing.state === 'READY') {
|
|
912
990
|
return existing;
|
|
913
991
|
}
|
|
914
992
|
// Check if userBridge has a client for this user (avoid duplicate connections)
|
|
915
|
-
// This prevents the connection storm where two clients fight for the same name
|
|
916
993
|
if (userBridge) {
|
|
917
994
|
const userBridgeClient = userBridge.getRelayClient(senderName);
|
|
918
995
|
if (userBridgeClient && userBridgeClient.state === 'READY') {
|
|
@@ -932,9 +1009,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
932
1009
|
}
|
|
933
1010
|
// Create connection promise to prevent race conditions
|
|
934
1011
|
const connectionPromise = (async () => {
|
|
935
|
-
// Create new client for this sender
|
|
936
|
-
// Default to 'user' entityType for non-Dashboard senders (human users)
|
|
937
|
-
// System clients (starting with '_') should NOT be users - they're internal clients
|
|
938
1012
|
const isSystemClient = senderName.startsWith('_') || senderName === 'Dashboard';
|
|
939
1013
|
const resolvedEntityType = entityType ?? (isSystemClient ? undefined : 'user');
|
|
940
1014
|
const client = new RelayClient({
|
|
@@ -944,7 +1018,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
944
1018
|
cli: 'dashboard',
|
|
945
1019
|
reconnect: true,
|
|
946
1020
|
maxReconnectAttempts: 5,
|
|
947
|
-
// Dashboard is a reserved name, so we need to mark it as a system component
|
|
948
1021
|
_isSystemComponent: senderName === 'Dashboard',
|
|
949
1022
|
});
|
|
950
1023
|
client.onError = (err) => {
|
|
@@ -952,28 +1025,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
952
1025
|
};
|
|
953
1026
|
client.onStateChange = (state) => {
|
|
954
1027
|
console.log(`[dashboard] Relay client for ${senderName} state: ${state}`);
|
|
955
|
-
// Handle state transitions
|
|
956
1028
|
if (state === 'READY') {
|
|
957
|
-
// Re-add to map on successful reconnection
|
|
958
1029
|
relayClients.set(senderName, client);
|
|
959
1030
|
}
|
|
960
1031
|
else if (state === 'DISCONNECTED') {
|
|
961
|
-
// Only remove if max reconnect attempts exhausted (client gives up)
|
|
962
|
-
// The client will auto-reconnect if configured, so don't remove prematurely
|
|
963
1032
|
relayClients.delete(senderName);
|
|
964
1033
|
}
|
|
965
1034
|
};
|
|
966
|
-
// Set up channel message handler to forward messages to presence WebSocket
|
|
967
|
-
// This enables cloud users to receive channel messages via the presence bridge
|
|
968
1035
|
client.onChannelMessage = (from, channel, body, envelope) => {
|
|
969
|
-
console.log(`[dashboard] *** CHANNEL MESSAGE RECEIVED *** for ${senderName}: ${from} -> ${channel}`);
|
|
970
|
-
// Look up sender's avatar from presence (if they're an online user)
|
|
971
1036
|
const senderPresence = onlineUsers.get(from);
|
|
972
1037
|
const fromAvatarUrl = senderPresence?.info.avatarUrl;
|
|
973
|
-
// Determine entity type: user if they have presence state, agent otherwise
|
|
974
1038
|
const fromEntityType = senderPresence ? 'user' : 'agent';
|
|
975
|
-
// Broadcast to presence WebSocket clients so cloud can forward to its users
|
|
976
|
-
// Include the target user so cloud knows who to forward to
|
|
977
1039
|
broadcastChannelMessage({
|
|
978
1040
|
type: 'channel_message',
|
|
979
1041
|
targetUser: senderName,
|
|
@@ -987,24 +1049,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
987
1049
|
timestamp: new Date().toISOString(),
|
|
988
1050
|
});
|
|
989
1051
|
};
|
|
990
|
-
// Set up direct message handler to forward messages to presence WebSocket
|
|
991
|
-
// This enables agents to send replies that appear in the dashboard UI
|
|
992
|
-
// Note: the relay daemon already persists messages authoritatively, so we
|
|
993
|
-
// only need to broadcast the real-time event here (no storage.saveMessage).
|
|
994
1052
|
client.onMessage = (from, payload, messageId) => {
|
|
995
1053
|
const body = typeof payload === 'object' && payload !== null && 'body' in payload
|
|
996
1054
|
? payload.body
|
|
997
1055
|
: String(payload);
|
|
998
|
-
console.log(`[dashboard] *** DIRECT MESSAGE RECEIVED *** for ${senderName}: ${from} -> ${senderName}: ${body.substring(0, 50)}...`);
|
|
999
|
-
// Look up sender's info from presence (if they're an online user)
|
|
1000
1056
|
const senderPresence = onlineUsers.get(from);
|
|
1001
1057
|
const fromAvatarUrl = senderPresence?.info.avatarUrl;
|
|
1002
|
-
// Determine entity type: user if they have presence state, agent otherwise
|
|
1003
1058
|
const fromEntityType = senderPresence ? 'user' : 'agent';
|
|
1004
1059
|
const timestamp = new Date().toISOString();
|
|
1005
|
-
// Broadcast real-time event so the dashboard UI updates immediately.
|
|
1006
|
-
// Pass id (= messageId) so the client has a stable identifier and
|
|
1007
|
-
// doesn't need to fabricate one from Date.now().
|
|
1008
1060
|
broadcastDirectMessage({
|
|
1009
1061
|
type: 'direct_message',
|
|
1010
1062
|
targetUser: senderName,
|
|
@@ -1028,54 +1080,96 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1028
1080
|
return undefined;
|
|
1029
1081
|
}
|
|
1030
1082
|
finally {
|
|
1031
|
-
// Clean up pending connection
|
|
1032
1083
|
pendingConnections.delete(senderName);
|
|
1033
1084
|
}
|
|
1034
1085
|
})();
|
|
1035
|
-
// Store the pending connection
|
|
1036
1086
|
pendingConnections.set(senderName, connectionPromise);
|
|
1037
1087
|
return connectionPromise;
|
|
1038
1088
|
};
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1089
|
+
// In broker mode, subscribe to relay_inbound events for message forwarding
|
|
1090
|
+
if (useBrokerAdapter) {
|
|
1091
|
+
relayAdapter.onEvent((event) => {
|
|
1092
|
+
if (event.kind === 'relay_inbound') {
|
|
1093
|
+
const senderPresence = onlineUsers.get(event.from);
|
|
1094
|
+
const fromAvatarUrl = senderPresence?.info.avatarUrl;
|
|
1095
|
+
const fromEntityType = senderPresence ? 'user' : 'agent';
|
|
1096
|
+
const timestamp = new Date().toISOString();
|
|
1097
|
+
// Route to channel or direct message based on target
|
|
1098
|
+
if (event.target.startsWith('#')) {
|
|
1099
|
+
broadcastChannelMessage({
|
|
1100
|
+
type: 'channel_message',
|
|
1101
|
+
targetUser: event.target,
|
|
1102
|
+
channel: event.target,
|
|
1103
|
+
from: event.from,
|
|
1104
|
+
fromAvatarUrl,
|
|
1105
|
+
fromEntityType,
|
|
1106
|
+
body: event.body,
|
|
1107
|
+
thread: event.thread_id,
|
|
1108
|
+
timestamp,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
broadcastDirectMessage({
|
|
1113
|
+
type: 'direct_message',
|
|
1114
|
+
targetUser: event.target,
|
|
1115
|
+
from: event.from,
|
|
1116
|
+
fromAvatarUrl,
|
|
1117
|
+
fromEntityType,
|
|
1118
|
+
body: event.body,
|
|
1119
|
+
id: event.event_id,
|
|
1120
|
+
messageId: event.event_id,
|
|
1121
|
+
timestamp,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1068
1124
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1125
|
+
});
|
|
1126
|
+
console.log('[dashboard] Broker event subscription active for message forwarding');
|
|
1127
|
+
}
|
|
1128
|
+
// Start default relay client connection (non-blocking) — skip in broker mode
|
|
1129
|
+
if (!useBrokerAdapter) {
|
|
1130
|
+
getRelayClient('Dashboard').catch(() => { });
|
|
1131
|
+
}
|
|
1132
|
+
// User bridge for human-to-human and human-to-agent messaging
|
|
1133
|
+
// In broker mode, skip UserBridge — messages go through the single adapter
|
|
1134
|
+
if (!useBrokerAdapter) {
|
|
1135
|
+
userBridge = new UserBridge({
|
|
1136
|
+
socketPath,
|
|
1137
|
+
createRelayClient: async (options) => {
|
|
1138
|
+
const client = new RelayClient({
|
|
1139
|
+
socketPath: options.socketPath,
|
|
1140
|
+
agentName: options.agentName,
|
|
1141
|
+
entityType: options.entityType,
|
|
1142
|
+
displayName: options.displayName,
|
|
1143
|
+
avatarUrl: options.avatarUrl,
|
|
1144
|
+
cli: 'dashboard',
|
|
1145
|
+
reconnect: true,
|
|
1146
|
+
maxReconnectAttempts: 5,
|
|
1147
|
+
});
|
|
1148
|
+
client.onError = (err) => {
|
|
1149
|
+
console.error(`[user-bridge] Relay client error for ${options.agentName}:`, err.message);
|
|
1150
|
+
};
|
|
1151
|
+
await client.connect();
|
|
1152
|
+
return client;
|
|
1153
|
+
},
|
|
1154
|
+
loadPersistedChannels: (username) => loadPersistedChannelsForUser(username, defaultWorkspaceId),
|
|
1155
|
+
lookupUserInfo: (username) => {
|
|
1156
|
+
const presence = onlineUsers.get(username);
|
|
1157
|
+
if (presence) {
|
|
1158
|
+
return { avatarUrl: presence.info.avatarUrl };
|
|
1159
|
+
}
|
|
1160
|
+
return undefined;
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1072
1164
|
// Bridge client for cross-project messaging
|
|
1165
|
+
// In broker mode, cross-project messaging goes through Relaycast cloud — skip socket bridge
|
|
1073
1166
|
let bridgeClient;
|
|
1074
1167
|
let bridgeClientConnecting = false;
|
|
1075
1168
|
const connectBridgeClient = async () => {
|
|
1169
|
+
if (useBrokerAdapter)
|
|
1170
|
+
return; // Cross-project messaging via Relaycast cloud in broker mode
|
|
1076
1171
|
if (bridgeClient || bridgeClientConnecting)
|
|
1077
1172
|
return;
|
|
1078
|
-
// Check if bridge-state.json exists and has projects
|
|
1079
1173
|
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
1080
1174
|
if (!fs.existsSync(bridgeStatePath)) {
|
|
1081
1175
|
return;
|
|
@@ -1086,28 +1180,25 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1086
1180
|
return;
|
|
1087
1181
|
}
|
|
1088
1182
|
bridgeClientConnecting = true;
|
|
1089
|
-
// Build project configs from bridge state
|
|
1090
1183
|
const projectConfigs = bridgeState.projects.map((p) => {
|
|
1091
|
-
// Compute socket path for each project
|
|
1092
1184
|
const projectHash = crypto.createHash('sha256').update(p.path).digest('hex').slice(0, 12);
|
|
1093
1185
|
const projectDataDir = path.join(path.dirname(dataDir), projectHash);
|
|
1094
|
-
const
|
|
1186
|
+
const projectSocketPath = path.join(projectDataDir, 'relay.sock');
|
|
1095
1187
|
return {
|
|
1096
1188
|
id: p.id,
|
|
1097
1189
|
path: p.path,
|
|
1098
|
-
socketPath,
|
|
1190
|
+
socketPath: projectSocketPath,
|
|
1099
1191
|
leadName: p.lead?.name || 'Lead',
|
|
1100
1192
|
cli: 'dashboard-bridge',
|
|
1101
1193
|
};
|
|
1102
1194
|
});
|
|
1103
|
-
// Filter to projects with existing sockets
|
|
1104
1195
|
const validConfigs = projectConfigs.filter((p) => fs.existsSync(p.socketPath));
|
|
1105
1196
|
if (validConfigs.length === 0) {
|
|
1106
1197
|
bridgeClientConnecting = false;
|
|
1107
1198
|
return;
|
|
1108
1199
|
}
|
|
1109
1200
|
bridgeClient = new MultiProjectClient(validConfigs, {
|
|
1110
|
-
agentName: '__DashboardBridge__',
|
|
1201
|
+
agentName: '__DashboardBridge__',
|
|
1111
1202
|
reconnect: true,
|
|
1112
1203
|
});
|
|
1113
1204
|
bridgeClient.onProjectStateChange = (projectId, connected) => {
|
|
@@ -1183,7 +1274,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1183
1274
|
if (username === '*')
|
|
1184
1275
|
return true;
|
|
1185
1276
|
// Check local presence, userBridge registration, and remote users from cloud
|
|
1186
|
-
return onlineUsers.has(username) || userBridge
|
|
1277
|
+
return onlineUsers.has(username) || userBridge?.isUserRegistered(username) || isRemoteUser(username);
|
|
1187
1278
|
};
|
|
1188
1279
|
const isRecipientOnline = (name) => (isAgentOnline(name) || isRemoteAgent(name) || isUserOnline(name));
|
|
1189
1280
|
// Helper to get team members from teams.json, agents.json, and spawner's active workers
|
|
@@ -2437,7 +2528,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2437
2528
|
console.error(`[dashboard] Error subscribing to ${agentName}:`, err);
|
|
2438
2529
|
});
|
|
2439
2530
|
}
|
|
2440
|
-
ws.on('message', (data) => {
|
|
2531
|
+
ws.on('message', async (data) => {
|
|
2441
2532
|
try {
|
|
2442
2533
|
const msg = JSON.parse(data.toString());
|
|
2443
2534
|
// Subscribe to agent logs
|
|
@@ -2480,7 +2571,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2480
2571
|
}
|
|
2481
2572
|
// Check if this is a spawned agent (we can only send input to spawned agents)
|
|
2482
2573
|
if (spawnReader?.hasWorker(agentName)) {
|
|
2483
|
-
const success = spawnReader.sendWorkerInput(agentName, msg.data);
|
|
2574
|
+
const success = await spawnReader.sendWorkerInput(agentName, msg.data);
|
|
2484
2575
|
if (!success) {
|
|
2485
2576
|
console.warn(`[dashboard] Failed to send input to agent ${agentName}`);
|
|
2486
2577
|
}
|
|
@@ -2730,7 +2821,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2730
2821
|
existing.info.lastSeen = now;
|
|
2731
2822
|
// Update userBridge to use the new WebSocket for message delivery
|
|
2732
2823
|
// This ensures messages are sent to an active connection, not a stale one
|
|
2733
|
-
userBridge
|
|
2824
|
+
userBridge?.updateWebSocket(username, ws);
|
|
2734
2825
|
// Only log at milestones to reduce noise
|
|
2735
2826
|
const count = existing.connections.size;
|
|
2736
2827
|
if (count === 2 || count === 5 || count === 10 || count % 50 === 0) {
|
|
@@ -2750,7 +2841,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2750
2841
|
});
|
|
2751
2842
|
console.log(`[dashboard] User ${username} came online`);
|
|
2752
2843
|
// Register user with relay daemon for messaging
|
|
2753
|
-
userBridge
|
|
2844
|
+
userBridge?.registerUser(username, ws, { avatarUrl }).catch((err) => {
|
|
2754
2845
|
console.error(`[dashboard] Failed to register user ${username} with relay:`, err);
|
|
2755
2846
|
});
|
|
2756
2847
|
// Broadcast join to all other clients (only for truly new users)
|
|
@@ -2836,7 +2927,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2836
2927
|
console.warn(`[dashboard] Invalid channel_join: missing channel`);
|
|
2837
2928
|
return;
|
|
2838
2929
|
}
|
|
2839
|
-
userBridge
|
|
2930
|
+
userBridge?.joinChannel(clientUsername, msg.channel).then((success) => {
|
|
2840
2931
|
ws.send(JSON.stringify({
|
|
2841
2932
|
type: 'channel_joined',
|
|
2842
2933
|
channel: msg.channel,
|
|
@@ -2862,7 +2953,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2862
2953
|
console.warn(`[dashboard] Invalid channel_leave: missing channel`);
|
|
2863
2954
|
return;
|
|
2864
2955
|
}
|
|
2865
|
-
userBridge
|
|
2956
|
+
userBridge?.leaveChannel(clientUsername, msg.channel).then((success) => {
|
|
2866
2957
|
ws.send(JSON.stringify({
|
|
2867
2958
|
type: 'channel_left',
|
|
2868
2959
|
channel: msg.channel,
|
|
@@ -2886,7 +2977,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2886
2977
|
console.warn(`[dashboard] Invalid channel_message: missing body`);
|
|
2887
2978
|
return;
|
|
2888
2979
|
}
|
|
2889
|
-
userBridge
|
|
2980
|
+
userBridge?.sendChannelMessage(clientUsername, msg.channel, msg.body, {
|
|
2890
2981
|
thread: msg.thread,
|
|
2891
2982
|
}).catch((err) => {
|
|
2892
2983
|
console.error(`[dashboard] Channel message error:`, err);
|
|
@@ -2906,7 +2997,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2906
2997
|
console.warn(`[dashboard] Invalid direct_message: missing body`);
|
|
2907
2998
|
return;
|
|
2908
2999
|
}
|
|
2909
|
-
userBridge
|
|
3000
|
+
userBridge?.sendDirectMessage(clientUsername, msg.to, msg.body, {
|
|
2910
3001
|
thread: msg.thread,
|
|
2911
3002
|
}).catch((err) => {
|
|
2912
3003
|
console.error(`[dashboard] Direct message error:`, err);
|
|
@@ -2931,7 +3022,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2931
3022
|
onlineUsers.delete(clientUsername);
|
|
2932
3023
|
console.log(`[dashboard] User ${clientUsername} disconnected`);
|
|
2933
3024
|
// Unregister from relay daemon
|
|
2934
|
-
userBridge
|
|
3025
|
+
userBridge?.unregisterUser(clientUsername);
|
|
2935
3026
|
broadcastPresence({
|
|
2936
3027
|
type: 'presence_leave',
|
|
2937
3028
|
username: clientUsername,
|
|
@@ -2964,7 +3055,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2964
3055
|
if (!username) {
|
|
2965
3056
|
return res.status(400).json({ error: 'username query param required' });
|
|
2966
3057
|
}
|
|
2967
|
-
const channels = userBridge
|
|
3058
|
+
const channels = userBridge?.getUserChannels(username) ?? [];
|
|
2968
3059
|
return res.json({
|
|
2969
3060
|
channels: channels.map((id) => ({
|
|
2970
3061
|
id,
|
|
@@ -3083,14 +3174,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3083
3174
|
// Join the creator to the channel
|
|
3084
3175
|
// Note: userBridge.joinChannel triggers router's persistChannelMembership via protocol
|
|
3085
3176
|
// We only persist here for dashboard-initiated creates (no daemon connection)
|
|
3086
|
-
await userBridge
|
|
3177
|
+
await userBridge?.joinChannel(username, channelId);
|
|
3087
3178
|
await persistChannelMembershipEvent(channelId, username, 'join', { workspaceId });
|
|
3088
3179
|
// Handle invites if provided
|
|
3089
3180
|
if (invites) {
|
|
3090
3181
|
const inviteList = invites.split(',').map((s) => s.trim()).filter(Boolean);
|
|
3091
3182
|
for (const invitee of inviteList) {
|
|
3092
3183
|
// userBridge.joinChannel handles persistence via protocol
|
|
3093
|
-
await userBridge
|
|
3184
|
+
await userBridge?.joinChannel(invitee, channelId);
|
|
3094
3185
|
await persistChannelMembershipEvent(channelId, invitee, 'invite', { invitedBy: username, workspaceId });
|
|
3095
3186
|
}
|
|
3096
3187
|
}
|
|
@@ -3176,8 +3267,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3176
3267
|
for (const invitee of inviteList) {
|
|
3177
3268
|
let success = false;
|
|
3178
3269
|
let reason;
|
|
3179
|
-
if (userBridge
|
|
3180
|
-
success = await userBridge
|
|
3270
|
+
if (userBridge?.isUserRegistered(invitee.id)) {
|
|
3271
|
+
success = await userBridge?.joinChannel(invitee.id, channelId);
|
|
3181
3272
|
if (!success) {
|
|
3182
3273
|
reason = 'join_failed';
|
|
3183
3274
|
}
|
|
@@ -3203,7 +3294,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3203
3294
|
* GET /api/channels/users - Get list of registered users
|
|
3204
3295
|
*/
|
|
3205
3296
|
app.get('/api/channels/users', (_req, res) => {
|
|
3206
|
-
const users = userBridge
|
|
3297
|
+
const users = userBridge?.getRegisteredUsers() ?? [];
|
|
3207
3298
|
res.json({ users });
|
|
3208
3299
|
});
|
|
3209
3300
|
/**
|
|
@@ -3223,11 +3314,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3223
3314
|
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
3224
3315
|
let success = false;
|
|
3225
3316
|
// Step 1: Try userBridge (for users connected via local WebSocket)
|
|
3226
|
-
const isLocalUser = userBridge
|
|
3317
|
+
const isLocalUser = userBridge?.isUserRegistered(username);
|
|
3227
3318
|
console.log(`[channels] Join: isLocalUser=${isLocalUser}`);
|
|
3228
3319
|
if (isLocalUser) {
|
|
3229
3320
|
console.log(`[channels] Calling userBridge.joinChannel(${username}, ${channelId})`);
|
|
3230
|
-
success = await userBridge
|
|
3321
|
+
success = await userBridge?.joinChannel(username, channelId) ?? false;
|
|
3231
3322
|
console.log(`[channels] userBridge.joinChannel returned: ${success}`);
|
|
3232
3323
|
}
|
|
3233
3324
|
// Step 2: If not local or userBridge failed, use relay client fallback
|
|
@@ -3264,7 +3355,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3264
3355
|
}
|
|
3265
3356
|
const workspaceId = resolveWorkspaceId(req);
|
|
3266
3357
|
try {
|
|
3267
|
-
const success = await userBridge
|
|
3358
|
+
const success = await userBridge?.leaveChannel(username, channel);
|
|
3268
3359
|
if (success) {
|
|
3269
3360
|
await persistChannelMembershipEvent(channel, username, 'leave', { workspaceId });
|
|
3270
3361
|
}
|
|
@@ -3286,7 +3377,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3286
3377
|
const workspaceId = resolveWorkspaceId(req);
|
|
3287
3378
|
try {
|
|
3288
3379
|
console.log(`[channels] Admin join: ${member} -> ${channel}`);
|
|
3289
|
-
const success = await userBridge
|
|
3380
|
+
const success = await userBridge?.adminJoinChannel(channel, member);
|
|
3290
3381
|
if (success) {
|
|
3291
3382
|
await persistChannelMembershipEvent(channel, member, 'join', { workspaceId });
|
|
3292
3383
|
}
|
|
@@ -3329,7 +3420,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3329
3420
|
const workspaceId = resolveWorkspaceId(req);
|
|
3330
3421
|
try {
|
|
3331
3422
|
console.log(`[channels] Admin remove: ${member} <- ${channel}`);
|
|
3332
|
-
const success = await userBridge
|
|
3423
|
+
const success = await userBridge?.adminRemoveMember(channel, member);
|
|
3333
3424
|
if (success) {
|
|
3334
3425
|
await persistChannelMembershipEvent(channel, member, 'leave', { workspaceId });
|
|
3335
3426
|
}
|
|
@@ -3368,7 +3459,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3368
3459
|
}
|
|
3369
3460
|
}
|
|
3370
3461
|
// Get connected users from userBridge
|
|
3371
|
-
const connectedUsers = userBridge
|
|
3462
|
+
const connectedUsers = userBridge?.getRegisteredUsers() ?? [];
|
|
3372
3463
|
// Build member list
|
|
3373
3464
|
const memberSet = new Set();
|
|
3374
3465
|
// Add persisted members
|
|
@@ -3502,7 +3593,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3502
3593
|
// This prevents falling back to getRelayClient which creates a conflicting connection
|
|
3503
3594
|
let regAttempts = 0;
|
|
3504
3595
|
const maxRegAttempts = 20; // 2 seconds max wait
|
|
3505
|
-
while (!userBridge
|
|
3596
|
+
while (!userBridge?.isUserRegistered(username) && regAttempts < maxRegAttempts) {
|
|
3506
3597
|
await new Promise(r => setTimeout(r, 100));
|
|
3507
3598
|
regAttempts++;
|
|
3508
3599
|
}
|
|
@@ -3512,13 +3603,13 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3512
3603
|
// Check if user is registered with userBridge (cloud users via presence)
|
|
3513
3604
|
// This is the preferred path as it uses the existing relay connection
|
|
3514
3605
|
// and ensures channel messages flow through the proper callback chain
|
|
3515
|
-
if (userBridge
|
|
3606
|
+
if (userBridge?.isUserRegistered(username)) {
|
|
3516
3607
|
console.log(`[channel-debug] SUBSCRIBE via userBridge for ${username}`);
|
|
3517
3608
|
for (const channel of channelList) {
|
|
3518
3609
|
const channelId = channel.startsWith('dm:')
|
|
3519
3610
|
? channel
|
|
3520
3611
|
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
3521
|
-
const joined = await userBridge
|
|
3612
|
+
const joined = await userBridge?.joinChannel(username, channelId);
|
|
3522
3613
|
if (joined) {
|
|
3523
3614
|
joinedChannels.push(channelId);
|
|
3524
3615
|
}
|
|
@@ -3593,16 +3684,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3593
3684
|
// For cloud-proxied requests, we need to use relay client directly
|
|
3594
3685
|
let success = false;
|
|
3595
3686
|
// Step 1: Check if user is registered with userBridge (local mode)
|
|
3596
|
-
const isLocalUser = userBridge
|
|
3687
|
+
const isLocalUser = userBridge?.isUserRegistered(username);
|
|
3597
3688
|
console.log(`[channel-msg] Is local user: ${isLocalUser}`);
|
|
3598
3689
|
if (isLocalUser) {
|
|
3599
3690
|
// Local user - use userBridge
|
|
3600
3691
|
console.log('[channel-msg] Using userBridge (local user)');
|
|
3601
|
-
success = await userBridge
|
|
3692
|
+
success = await userBridge?.sendChannelMessage(username, channelId, body, {
|
|
3602
3693
|
thread,
|
|
3603
3694
|
data: workspaceId ? { _workspaceId: workspaceId } : undefined,
|
|
3604
3695
|
attachments,
|
|
3605
|
-
});
|
|
3696
|
+
}) ?? false;
|
|
3606
3697
|
console.log(`[channel-msg] userBridge result: ${success}`);
|
|
3607
3698
|
}
|
|
3608
3699
|
// Step 2: If not local or userBridge failed, use relay client
|
|
@@ -3715,7 +3806,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3715
3806
|
return res.status(400).json({ error: 'from, to, and body required' });
|
|
3716
3807
|
}
|
|
3717
3808
|
try {
|
|
3718
|
-
const success = await userBridge
|
|
3809
|
+
const success = await userBridge?.sendDirectMessage(from, to, body, { thread });
|
|
3719
3810
|
res.json({ success });
|
|
3720
3811
|
}
|
|
3721
3812
|
catch (err) {
|
|
@@ -4968,9 +5059,24 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4968
5059
|
const effectiveCwd = cwd || (spawnerName ? agentCwdMap.get(spawnerName) : undefined);
|
|
4969
5060
|
try {
|
|
4970
5061
|
let result;
|
|
4971
|
-
if (
|
|
5062
|
+
if (useBrokerAdapter) {
|
|
5063
|
+
// Route spawn through broker SDK → Rust binary
|
|
5064
|
+
result = await relayAdapter.spawn({
|
|
5065
|
+
name,
|
|
5066
|
+
cli,
|
|
5067
|
+
task,
|
|
5068
|
+
team: team || undefined,
|
|
5069
|
+
cwd: effectiveCwd || undefined,
|
|
5070
|
+
interactive,
|
|
5071
|
+
shadowMode,
|
|
5072
|
+
shadowOf,
|
|
5073
|
+
spawnerName: spawnerName || undefined,
|
|
5074
|
+
userId: typeof userId === 'string' ? userId : undefined,
|
|
5075
|
+
includeWorkflowConventions: true,
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
else if (useExternalSpawnManager) {
|
|
4972
5079
|
// Route spawn through SDK → daemon socket → SpawnManager
|
|
4973
|
-
// This ensures relay-pty binary resolution works regardless of install method
|
|
4974
5080
|
const client = await getRelayClient('Dashboard');
|
|
4975
5081
|
if (!client) {
|
|
4976
5082
|
return res.status(503).json({
|
|
@@ -4978,8 +5084,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4978
5084
|
error: 'Not connected to relay daemon',
|
|
4979
5085
|
});
|
|
4980
5086
|
}
|
|
4981
|
-
// spawnerName/userId/includeWorkflowConventions need SDK >= 2.2.0 types
|
|
4982
|
-
// Cast removed once SDK is republished with these fields
|
|
4983
5087
|
result = await client.spawn({
|
|
4984
5088
|
name,
|
|
4985
5089
|
cli,
|
|
@@ -5236,7 +5340,15 @@ Then output: \`->relay-file:all\`
|
|
|
5236
5340
|
Start by greeting the project leads and asking for status updates.`;
|
|
5237
5341
|
try {
|
|
5238
5342
|
let result;
|
|
5239
|
-
if (
|
|
5343
|
+
if (useBrokerAdapter) {
|
|
5344
|
+
result = await relayAdapter.spawn({
|
|
5345
|
+
name: 'Architect',
|
|
5346
|
+
cli,
|
|
5347
|
+
task: architectPrompt,
|
|
5348
|
+
includeWorkflowConventions: true,
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
else if (useExternalSpawnManager) {
|
|
5240
5352
|
const client = await getRelayClient('Dashboard');
|
|
5241
5353
|
if (!client) {
|
|
5242
5354
|
return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
|
|
@@ -5343,7 +5455,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5343
5455
|
* DELETE /api/spawned/:name - Release a spawned agent
|
|
5344
5456
|
*/
|
|
5345
5457
|
app.delete('/api/spawned/:name', async (req, res) => {
|
|
5346
|
-
if (!spawner && !useExternalSpawnManager) {
|
|
5458
|
+
if (!spawner && !useExternalSpawnManager && !useBrokerAdapter) {
|
|
5347
5459
|
return res.status(503).json({
|
|
5348
5460
|
success: false,
|
|
5349
5461
|
error: 'Spawner not enabled',
|
|
@@ -5352,8 +5464,11 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5352
5464
|
const { name } = req.params;
|
|
5353
5465
|
try {
|
|
5354
5466
|
let released;
|
|
5355
|
-
if (
|
|
5356
|
-
|
|
5467
|
+
if (useBrokerAdapter) {
|
|
5468
|
+
const result = await relayAdapter.release(name);
|
|
5469
|
+
released = result.success;
|
|
5470
|
+
}
|
|
5471
|
+
else if (useExternalSpawnManager) {
|
|
5357
5472
|
const client = await getRelayClient('Dashboard');
|
|
5358
5473
|
if (!client) {
|
|
5359
5474
|
return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
|
|
@@ -5396,7 +5511,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5396
5511
|
* Sends ESC ESC (0x1b 0x1b) to the agent's PTY to interrupt the current operation.
|
|
5397
5512
|
* This is useful for breaking agents out of stuck loops without terminating them.
|
|
5398
5513
|
*/
|
|
5399
|
-
app.post('/api/agents/by-name/:name/interrupt', (req, res) => {
|
|
5514
|
+
app.post('/api/agents/by-name/:name/interrupt', async (req, res) => {
|
|
5400
5515
|
if (!spawnReader) {
|
|
5401
5516
|
return res.status(503).json({
|
|
5402
5517
|
success: false,
|
|
@@ -5414,7 +5529,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5414
5529
|
try {
|
|
5415
5530
|
// Send ESC ESC sequence to interrupt the agent
|
|
5416
5531
|
// ESC = 0x1b in hexadecimal
|
|
5417
|
-
const success = spawnReader.sendWorkerInput(name, '\x1b\x1b');
|
|
5532
|
+
const success = await spawnReader.sendWorkerInput(name, '\x1b\x1b');
|
|
5418
5533
|
if (success) {
|
|
5419
5534
|
console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`);
|
|
5420
5535
|
res.json({
|