@agent-relay/dashboard-server 2.0.63 → 2.0.65
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/messageBuffer.d.ts +38 -0
- package/dist/messageBuffer.d.ts.map +1 -0
- package/dist/messageBuffer.js +72 -0
- package/dist/messageBuffer.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +149 -23
- package/dist/server.js.map +1 -1
- package/out/404.html +1 -1
- package/out/_next/static/chunks/535-757cbf5de3af1d18.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-a528040db9d1fec0.js → page-7c9abc28789ea7cb.js} +1 -1
- package/out/_next/static/chunks/app/{page-a32b25323fff7aa0.js → page-ba281b017e148cd6.js} +1 -1
- package/out/_next/static/css/15362c88976df1b9.css +1 -0
- package/out/about.html +2 -2
- 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 +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- 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 +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- 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 +2 -2
- 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 +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- 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 +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +1 -1
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- 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 +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +10 -10
- package/out/_next/static/chunks/873-6b31247a84ec58c2.js +0 -1
- package/out/_next/static/css/ad96af0f7a47b705.css +0 -1
- /package/out/_next/static/{Ip08bs-aI4i94zrABOaVi → XAoBjrJ3N72573Ty4Ja_J}/_buildManifest.js +0 -0
- /package/out/_next/static/{Ip08bs-aI4i94zrABOaVi → XAoBjrJ3N72573Ty4Ja_J}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer for storing recent WebSocket messages.
|
|
3
|
+
* Used to replay missed messages when clients reconnect after brief disconnects.
|
|
4
|
+
*/
|
|
5
|
+
export interface BufferedMessage {
|
|
6
|
+
id: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
type: string;
|
|
9
|
+
payload: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class MessageBuffer {
|
|
12
|
+
private buffer;
|
|
13
|
+
private capacity;
|
|
14
|
+
private writeIndex;
|
|
15
|
+
private sequenceCounter;
|
|
16
|
+
constructor(capacity?: number);
|
|
17
|
+
/**
|
|
18
|
+
* Push a new message into the buffer.
|
|
19
|
+
* Returns the assigned sequence ID.
|
|
20
|
+
*/
|
|
21
|
+
push(type: string, payload: string): number;
|
|
22
|
+
/**
|
|
23
|
+
* Get all messages with an ID greater than the given sequence ID.
|
|
24
|
+
* Returns messages in chronological order.
|
|
25
|
+
*/
|
|
26
|
+
getAfter(sequenceId: number): BufferedMessage[];
|
|
27
|
+
/**
|
|
28
|
+
* Get all messages with a timestamp greater than the given timestamp.
|
|
29
|
+
* Returns messages in chronological order.
|
|
30
|
+
*/
|
|
31
|
+
getAfterTimestamp(ts: number): BufferedMessage[];
|
|
32
|
+
/**
|
|
33
|
+
* Get the current sequence ID (the ID of the last pushed message).
|
|
34
|
+
* Returns 0 if no messages have been pushed.
|
|
35
|
+
*/
|
|
36
|
+
currentId(): number;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=messageBuffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messageBuffer.d.ts","sourceRoot":"","sources":["../src/messageBuffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAS;gBAEpB,QAAQ,GAAE,MAAY;IAOlC;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAa3C;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,EAAE;IAa/C;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,EAAE;IAahD;;;OAGG;IACH,SAAS,IAAI,MAAM;CAGpB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer for storing recent WebSocket messages.
|
|
3
|
+
* Used to replay missed messages when clients reconnect after brief disconnects.
|
|
4
|
+
*/
|
|
5
|
+
export class MessageBuffer {
|
|
6
|
+
buffer;
|
|
7
|
+
capacity;
|
|
8
|
+
writeIndex;
|
|
9
|
+
sequenceCounter;
|
|
10
|
+
constructor(capacity = 500) {
|
|
11
|
+
this.capacity = capacity;
|
|
12
|
+
this.buffer = new Array(capacity).fill(null);
|
|
13
|
+
this.writeIndex = 0;
|
|
14
|
+
this.sequenceCounter = 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Push a new message into the buffer.
|
|
18
|
+
* Returns the assigned sequence ID.
|
|
19
|
+
*/
|
|
20
|
+
push(type, payload) {
|
|
21
|
+
this.sequenceCounter++;
|
|
22
|
+
const message = {
|
|
23
|
+
id: this.sequenceCounter,
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
type,
|
|
26
|
+
payload,
|
|
27
|
+
};
|
|
28
|
+
this.buffer[this.writeIndex] = message;
|
|
29
|
+
this.writeIndex = (this.writeIndex + 1) % this.capacity;
|
|
30
|
+
return this.sequenceCounter;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get all messages with an ID greater than the given sequence ID.
|
|
34
|
+
* Returns messages in chronological order.
|
|
35
|
+
*/
|
|
36
|
+
getAfter(sequenceId) {
|
|
37
|
+
const results = [];
|
|
38
|
+
for (let i = 0; i < this.capacity; i++) {
|
|
39
|
+
const msg = this.buffer[i];
|
|
40
|
+
if (msg && msg.id > sequenceId) {
|
|
41
|
+
results.push(msg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Sort by id to ensure chronological order
|
|
45
|
+
results.sort((a, b) => a.id - b.id);
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get all messages with a timestamp greater than the given timestamp.
|
|
50
|
+
* Returns messages in chronological order.
|
|
51
|
+
*/
|
|
52
|
+
getAfterTimestamp(ts) {
|
|
53
|
+
const results = [];
|
|
54
|
+
for (let i = 0; i < this.capacity; i++) {
|
|
55
|
+
const msg = this.buffer[i];
|
|
56
|
+
if (msg && msg.timestamp > ts) {
|
|
57
|
+
results.push(msg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Sort by id to ensure chronological order
|
|
61
|
+
results.sort((a, b) => a.id - b.id);
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the current sequence ID (the ID of the last pushed message).
|
|
66
|
+
* Returns 0 if no messages have been pushed.
|
|
67
|
+
*/
|
|
68
|
+
currentId() {
|
|
69
|
+
return this.sequenceCounter;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=messageBuffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messageBuffer.js","sourceRoot":"","sources":["../src/messageBuffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,OAAO,aAAa;IAChB,MAAM,CAA6B;IACnC,QAAQ,CAAS;IACjB,UAAU,CAAS;IACnB,eAAe,CAAS;IAEhC,YAAY,WAAmB,GAAG;QAChC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,IAAY,EAAE,OAAe;QAChC,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,MAAM,OAAO,GAAoB;YAC/B,EAAE,EAAE,IAAI,CAAC,eAAe;YACxB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,IAAI;YACJ,OAAO;SACR,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC;QACvC,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QACxD,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,UAAkB;QACzB,MAAM,OAAO,GAAsB,EAAE,CAAC;QACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC;gBAC/B,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QACD,2CAA2C;QAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,EAAU;QAC1B,MAAM,OAAO,GAAsB,EAAE,CAAC;QACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC;gBAC9B,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QACD,2CAA2C;QAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,SAAS;QACP,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;CACF"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA8DA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA8DA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAkYzD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AACvH,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -55,6 +55,7 @@ function findDashboardDir() {
|
|
|
55
55
|
}
|
|
56
56
|
import { startCLIAuth, getAuthSession, cancelAuthSession, submitAuthCode, completeAuthSession, getSupportedProviders, } from '@agent-relay/daemon';
|
|
57
57
|
import { HealthWorkerManager, getHealthPort } from './services/health-worker-manager.js';
|
|
58
|
+
import { MessageBuffer } from './messageBuffer.js';
|
|
58
59
|
/**
|
|
59
60
|
* Get the host to bind to.
|
|
60
61
|
* In cloud environments, bind to '::' (IPv6 any) which also accepts IPv4 on dual-stack.
|
|
@@ -480,8 +481,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
480
481
|
});
|
|
481
482
|
};
|
|
482
483
|
// Initialize spawner if enabled
|
|
483
|
-
//
|
|
484
|
-
|
|
484
|
+
// When projectRoot is explicitly provided (e.g., via --project-root), use it directly.
|
|
485
|
+
// Only use detectWorkspacePath for cloud workspace auto-detection when no explicit root is given.
|
|
486
|
+
// This fixes #380: detectWorkspacePath could re-resolve projectRoot incorrectly when
|
|
487
|
+
// tool directories like ~/.nvm contain package.json markers.
|
|
488
|
+
const workspacePath = projectRoot || detectWorkspacePath(dataDir);
|
|
485
489
|
console.log(`[dashboard] Workspace path: ${workspacePath}`);
|
|
486
490
|
// When an external SpawnManager is provided (from the daemon), use it for read operations
|
|
487
491
|
// (logs, worker listing, hasWorker) and route spawn/release through the SDK client.
|
|
@@ -581,14 +585,28 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
581
585
|
// Track file watchers for externally-spawned worker logs (module scope to avoid duplicates)
|
|
582
586
|
const fileWatchers = new Map();
|
|
583
587
|
const fileLastSize = new Map();
|
|
588
|
+
// Message buffers for replay on reconnect
|
|
589
|
+
// Main buffer stores broadcast messages for the main dashboard WebSocket
|
|
590
|
+
const mainMessageBuffer = new MessageBuffer(500);
|
|
591
|
+
// Per-agent log buffers store log output for each agent (smaller capacity since per-agent)
|
|
592
|
+
const agentLogBuffers = new Map();
|
|
593
|
+
/** Get or create a log buffer for an agent */
|
|
594
|
+
const getAgentLogBuffer = (agentName) => {
|
|
595
|
+
let buffer = agentLogBuffers.get(agentName);
|
|
596
|
+
if (!buffer) {
|
|
597
|
+
buffer = new MessageBuffer(200);
|
|
598
|
+
agentLogBuffers.set(agentName, buffer);
|
|
599
|
+
}
|
|
600
|
+
return buffer;
|
|
601
|
+
};
|
|
584
602
|
// Track alive status for ping/pong keepalive on main dashboard connections
|
|
585
603
|
// This prevents TCP/proxy timeouts from killing idle workspace connections
|
|
586
604
|
const mainClientAlive = new WeakMap();
|
|
587
605
|
// Track alive status for ping/pong keepalive on bridge connections
|
|
588
606
|
const bridgeClientAlive = new WeakMap();
|
|
589
|
-
// Ping interval for main dashboard WebSocket connections (
|
|
590
|
-
//
|
|
591
|
-
const MAIN_PING_INTERVAL_MS =
|
|
607
|
+
// Ping interval for main dashboard WebSocket connections (15 seconds)
|
|
608
|
+
// Reduced from 30s to detect disconnects faster and minimize message loss window
|
|
609
|
+
const MAIN_PING_INTERVAL_MS = 15000;
|
|
592
610
|
const mainPingInterval = setInterval(() => {
|
|
593
611
|
wss.clients.forEach((ws) => {
|
|
594
612
|
if (mainClientAlive.get(ws) === false) {
|
|
@@ -602,8 +620,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
602
620
|
ws.ping();
|
|
603
621
|
});
|
|
604
622
|
}, MAIN_PING_INTERVAL_MS);
|
|
605
|
-
// Ping interval for bridge WebSocket connections (
|
|
606
|
-
|
|
623
|
+
// Ping interval for bridge WebSocket connections (15 seconds)
|
|
624
|
+
// Reduced from 30s to detect disconnects faster and minimize message loss window
|
|
625
|
+
const BRIDGE_PING_INTERVAL_MS = 15000;
|
|
607
626
|
const bridgePingInterval = setInterval(() => {
|
|
608
627
|
wssBridge.clients.forEach((ws) => {
|
|
609
628
|
if (bridgeClientAlive.get(ws) === false) {
|
|
@@ -790,24 +809,43 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
790
809
|
// Serve Next.js static export with .html extension handling
|
|
791
810
|
app.use(express.static(dashboardDir, { extensions: ['html'] }));
|
|
792
811
|
// Fallback for Next.js pages (e.g., /metrics -> /metrics.html)
|
|
793
|
-
// These are needed when a route exists as both a directory and .html file
|
|
794
|
-
|
|
812
|
+
// These are needed when a route exists as both a directory and .html file.
|
|
813
|
+
// For /app/* deep links we prefer redirecting to "/" if the export is missing,
|
|
814
|
+
// so users don’t get stuck on a plain-text error on refresh.
|
|
815
|
+
const uiMissingMessage = 'Dashboard UI file not found. Please reinstall using: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash';
|
|
816
|
+
const sendFileOr = (res, filePath, onError) => {
|
|
795
817
|
res.sendFile(filePath, (err) => {
|
|
796
818
|
if (err && !res.headersSent) {
|
|
797
|
-
|
|
819
|
+
onError(err);
|
|
798
820
|
}
|
|
799
821
|
});
|
|
800
822
|
};
|
|
823
|
+
const sendFileOrText404 = (res, filePath, message) => {
|
|
824
|
+
sendFileOr(res, filePath, () => {
|
|
825
|
+
res.status(404).send(message);
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
const sendFileOrRedirectRoot = (res, filePath) => {
|
|
829
|
+
sendFileOr(res, filePath, () => {
|
|
830
|
+
// If the app entrypoint isn’t present, try to recover by sending users
|
|
831
|
+
// to the root page (if it exists). Otherwise keep the install hint.
|
|
832
|
+
if (fs.existsSync(path.join(dashboardDir, 'index.html'))) {
|
|
833
|
+
res.redirect(302, '/');
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
res.status(404).send(uiMissingMessage);
|
|
837
|
+
});
|
|
838
|
+
};
|
|
801
839
|
app.get('/metrics', (req, res) => {
|
|
802
|
-
|
|
840
|
+
sendFileOrText404(res, path.join(dashboardDir, 'metrics.html'), uiMissingMessage);
|
|
803
841
|
});
|
|
804
842
|
app.get('/app', (req, res) => {
|
|
805
|
-
|
|
843
|
+
sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
|
|
806
844
|
});
|
|
807
845
|
// Catch-all for /app/* routes - serve app.html and let client-side routing handle it
|
|
808
846
|
// Express 5 requires named parameter for wildcards
|
|
809
847
|
app.get('/app/{*path}', (req, res) => {
|
|
810
|
-
|
|
848
|
+
sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
|
|
811
849
|
});
|
|
812
850
|
}
|
|
813
851
|
else {
|
|
@@ -1796,7 +1834,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1796
1834
|
// Ignore errors reading processing state - it's optional
|
|
1797
1835
|
}
|
|
1798
1836
|
}
|
|
1799
|
-
// Mark spawned agents with isSpawned flag and
|
|
1837
|
+
// Mark spawned agents with isSpawned flag, team, and model
|
|
1800
1838
|
if (spawnReader) {
|
|
1801
1839
|
const activeWorkers = spawnReader.getActiveWorkers();
|
|
1802
1840
|
for (const worker of activeWorkers) {
|
|
@@ -1806,6 +1844,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1806
1844
|
if (worker.team) {
|
|
1807
1845
|
agent.team = worker.team;
|
|
1808
1846
|
}
|
|
1847
|
+
// Extract model from spawn command (e.g., "codex --model gpt-5.2-codex" → "gpt-5.2-codex")
|
|
1848
|
+
if (worker.cli) {
|
|
1849
|
+
// Support both `--model foo` and `--model=foo`
|
|
1850
|
+
const modelMatch = worker.cli.match(/--model[=\s]+(\S+)/);
|
|
1851
|
+
if (modelMatch) {
|
|
1852
|
+
agent.model = modelMatch[1];
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1809
1855
|
}
|
|
1810
1856
|
}
|
|
1811
1857
|
}
|
|
@@ -1903,12 +1949,15 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1903
1949
|
const broadcastData = async () => {
|
|
1904
1950
|
try {
|
|
1905
1951
|
const data = await getAllData();
|
|
1906
|
-
const
|
|
1952
|
+
const rawPayload = JSON.stringify(data);
|
|
1907
1953
|
// Guard against empty/invalid payloads
|
|
1908
|
-
if (!
|
|
1954
|
+
if (!rawPayload || rawPayload.length === 0) {
|
|
1909
1955
|
console.warn('[dashboard] Skipping broadcast - empty payload');
|
|
1910
1956
|
return;
|
|
1911
1957
|
}
|
|
1958
|
+
// Push into buffer and wrap with sequence ID for replay support
|
|
1959
|
+
const seq = mainMessageBuffer.push('data', rawPayload);
|
|
1960
|
+
const payload = JSON.stringify({ seq, ...data });
|
|
1912
1961
|
wss.clients.forEach(client => {
|
|
1913
1962
|
// Skip clients that are still being initialized by the connection handler
|
|
1914
1963
|
if (initializingClients.has(client)) {
|
|
@@ -2018,6 +2067,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2018
2067
|
ws.on('pong', () => {
|
|
2019
2068
|
mainClientAlive.set(ws, true);
|
|
2020
2069
|
});
|
|
2070
|
+
// Send current sequence ID so client can track its position
|
|
2071
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2072
|
+
ws.send(JSON.stringify({ type: 'sync', sequenceId: mainMessageBuffer.currentId() }));
|
|
2073
|
+
}
|
|
2021
2074
|
// Mark as initializing to prevent broadcastData from sending before we do
|
|
2022
2075
|
initializingClients.add(ws);
|
|
2023
2076
|
try {
|
|
@@ -2044,6 +2097,39 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2044
2097
|
// Now allow broadcastData to send to this client
|
|
2045
2098
|
initializingClients.delete(ws);
|
|
2046
2099
|
}
|
|
2100
|
+
// Handle messages from client (replay requests, etc.)
|
|
2101
|
+
ws.on('message', (data) => {
|
|
2102
|
+
try {
|
|
2103
|
+
const msg = JSON.parse(data.toString());
|
|
2104
|
+
// Handle replay request: client sends { type: "replay", lastSequenceId: N }
|
|
2105
|
+
if (msg.type === 'replay' && typeof msg.lastSequenceId === 'number') {
|
|
2106
|
+
const missed = mainMessageBuffer.getAfter(msg.lastSequenceId);
|
|
2107
|
+
const gapMs = missed.length > 0 ? Date.now() - missed[0].timestamp : 0;
|
|
2108
|
+
console.log(`[dashboard] Client replaying ${missed.length} missed messages (gap: ${gapMs}ms)`);
|
|
2109
|
+
// Send each missed message with its original sequence ID
|
|
2110
|
+
for (const buffered of missed) {
|
|
2111
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2112
|
+
try {
|
|
2113
|
+
// Reconstruct the payload with the seq wrapper
|
|
2114
|
+
const original = JSON.parse(buffered.payload);
|
|
2115
|
+
ws.send(JSON.stringify({ seq: buffered.id, ...original }));
|
|
2116
|
+
}
|
|
2117
|
+
catch (err) {
|
|
2118
|
+
console.error('[dashboard] Failed to replay message:', err);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
// Send current sync position after replay
|
|
2123
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2124
|
+
ws.send(JSON.stringify({ type: 'sync', sequenceId: mainMessageBuffer.currentId() }));
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
catch (err) {
|
|
2129
|
+
// Non-JSON messages are ignored (binary, etc.)
|
|
2130
|
+
debug(`[dashboard] Unhandled main WebSocket message: ${err}`);
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2047
2133
|
ws.on('error', (err) => {
|
|
2048
2134
|
console.error('[dashboard] WebSocket client error:', err);
|
|
2049
2135
|
});
|
|
@@ -2079,9 +2165,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2079
2165
|
});
|
|
2080
2166
|
// Track alive status for ping/pong keepalive on log connections
|
|
2081
2167
|
const logClientAlive = new WeakMap();
|
|
2082
|
-
// Ping interval for log WebSocket connections (
|
|
2083
|
-
//
|
|
2084
|
-
const LOG_PING_INTERVAL_MS =
|
|
2168
|
+
// Ping interval for log WebSocket connections (15 seconds)
|
|
2169
|
+
// Reduced from 30s to detect disconnects faster and minimize message loss window
|
|
2170
|
+
const LOG_PING_INTERVAL_MS = 15000;
|
|
2085
2171
|
const logPingInterval = setInterval(() => {
|
|
2086
2172
|
wssLogs.clients.forEach((ws) => {
|
|
2087
2173
|
if (logClientAlive.get(ws) === false) {
|
|
@@ -2109,6 +2195,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2109
2195
|
ws.on('pong', () => {
|
|
2110
2196
|
logClientAlive.set(ws, true);
|
|
2111
2197
|
});
|
|
2198
|
+
// Send sync message with current server timestamp so client can track its position
|
|
2199
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2200
|
+
ws.send(JSON.stringify({ type: 'sync', serverTimestamp: Date.now() }));
|
|
2201
|
+
}
|
|
2112
2202
|
// Helper to check if agent is daemon-connected (from agents.json)
|
|
2113
2203
|
const isDaemonConnected = (agentName) => {
|
|
2114
2204
|
const agentsPath = path.join(teamDir, 'agents.json');
|
|
@@ -2188,6 +2278,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2188
2278
|
data: newContent,
|
|
2189
2279
|
timestamp: new Date().toISOString(),
|
|
2190
2280
|
});
|
|
2281
|
+
// Push into per-agent log buffer for replay on reconnect
|
|
2282
|
+
getAgentLogBuffer(agentName).push('output', payload);
|
|
2191
2283
|
for (const client of clients) {
|
|
2192
2284
|
if (client.readyState === WebSocket.OPEN) {
|
|
2193
2285
|
client.send(payload);
|
|
@@ -2362,6 +2454,29 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2362
2454
|
}));
|
|
2363
2455
|
}
|
|
2364
2456
|
}
|
|
2457
|
+
// Handle replay request: client sends { type: "replay", agent: "name", lastTimestamp: N }
|
|
2458
|
+
// Logs use timestamps instead of sequence IDs since the data is raw text
|
|
2459
|
+
if (msg.type === 'replay' && typeof msg.agent === 'string' && typeof msg.lastTimestamp === 'number') {
|
|
2460
|
+
const logBuffer = agentLogBuffers.get(msg.agent);
|
|
2461
|
+
if (logBuffer) {
|
|
2462
|
+
const missed = logBuffer.getAfterTimestamp(msg.lastTimestamp);
|
|
2463
|
+
const gapMs = missed.length > 0 ? Date.now() - missed[0].timestamp : 0;
|
|
2464
|
+
console.log(`[dashboard] Client replaying ${missed.length} missed log messages for ${msg.agent} (gap: ${gapMs}ms)`);
|
|
2465
|
+
// Send replay as a structured response the client expects
|
|
2466
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2467
|
+
try {
|
|
2468
|
+
const entries = missed.map(m => ({
|
|
2469
|
+
content: m.payload,
|
|
2470
|
+
timestamp: m.timestamp,
|
|
2471
|
+
}));
|
|
2472
|
+
ws.send(JSON.stringify({ type: 'replay', entries }));
|
|
2473
|
+
}
|
|
2474
|
+
catch (err) {
|
|
2475
|
+
console.error('[dashboard] Failed to replay log messages:', err);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2365
2480
|
}
|
|
2366
2481
|
catch (err) {
|
|
2367
2482
|
console.error('[dashboard] Invalid logs WebSocket message:', err);
|
|
@@ -2382,6 +2497,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2382
2497
|
watcher.close();
|
|
2383
2498
|
fileWatchers.delete(agentName);
|
|
2384
2499
|
fileLastSize.delete(agentName);
|
|
2500
|
+
agentLogBuffers.delete(agentName);
|
|
2385
2501
|
console.log(`[dashboard] Stopped watching log file for: ${agentName}`);
|
|
2386
2502
|
}
|
|
2387
2503
|
}
|
|
@@ -2435,12 +2551,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2435
2551
|
agentHashes.delete(oldest);
|
|
2436
2552
|
}
|
|
2437
2553
|
}
|
|
2438
|
-
const
|
|
2554
|
+
const logPayload = {
|
|
2439
2555
|
type: 'output',
|
|
2440
2556
|
agent: agentName,
|
|
2441
2557
|
data: output,
|
|
2442
2558
|
timestamp: new Date().toISOString(),
|
|
2443
|
-
}
|
|
2559
|
+
};
|
|
2560
|
+
const payload = JSON.stringify(logPayload);
|
|
2561
|
+
// Push into per-agent log buffer for replay on reconnect
|
|
2562
|
+
// Logs use timestamps instead of sequence IDs since the data is raw text
|
|
2563
|
+
getAgentLogBuffer(agentName).push('output', payload);
|
|
2444
2564
|
for (const client of clients) {
|
|
2445
2565
|
if (client.readyState === WebSocket.OPEN) {
|
|
2446
2566
|
client.send(payload);
|
|
@@ -2462,7 +2582,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2462
2582
|
// Helper to broadcast channel messages to all connected clients
|
|
2463
2583
|
// Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
|
|
2464
2584
|
const broadcastChannelMessage = (message) => {
|
|
2465
|
-
|
|
2585
|
+
// Push into buffer and wrap with sequence ID for replay support
|
|
2586
|
+
const rawPayload = JSON.stringify(message);
|
|
2587
|
+
const seq = mainMessageBuffer.push('channel_message', rawPayload);
|
|
2588
|
+
const payload = JSON.stringify({ seq, ...message });
|
|
2466
2589
|
// Broadcast to main WebSocket clients (local mode)
|
|
2467
2590
|
wss.clients.forEach((client) => {
|
|
2468
2591
|
if (client.readyState === WebSocket.OPEN) {
|
|
@@ -2480,7 +2603,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2480
2603
|
// This enables agent replies to appear in the dashboard UI
|
|
2481
2604
|
// Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
|
|
2482
2605
|
const broadcastDirectMessage = (message) => {
|
|
2483
|
-
|
|
2606
|
+
// Push into buffer and wrap with sequence ID for replay support
|
|
2607
|
+
const rawPayload = JSON.stringify(message);
|
|
2608
|
+
const seq = mainMessageBuffer.push('direct_message', rawPayload);
|
|
2609
|
+
const payload = JSON.stringify({ seq, ...message });
|
|
2484
2610
|
// Broadcast to main WebSocket clients (local mode)
|
|
2485
2611
|
const mainClients = Array.from(wss.clients).filter(c => c.readyState === WebSocket.OPEN);
|
|
2486
2612
|
debug(`[dashboard] Broadcasting direct_message to ${mainClients.length} main clients`);
|