@agent-relay/dashboard-server 2.0.64 → 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 +117 -17
- 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 +1 -1
- package/out/_next/static/chunks/873-9aee36b975a9556a.js +0 -1
- package/out/_next/static/css/2ee05ba949b3ac9f.css +0 -1
- /package/out/_next/static/{6oI4iquYj1QbK8njLsK3s → XAoBjrJ3N72573Ty4Ja_J}/_buildManifest.js +0 -0
- /package/out/_next/static/{6oI4iquYj1QbK8njLsK3s → 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) {
|
|
@@ -1827,7 +1846,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1827
1846
|
}
|
|
1828
1847
|
// Extract model from spawn command (e.g., "codex --model gpt-5.2-codex" → "gpt-5.2-codex")
|
|
1829
1848
|
if (worker.cli) {
|
|
1830
|
-
|
|
1849
|
+
// Support both `--model foo` and `--model=foo`
|
|
1850
|
+
const modelMatch = worker.cli.match(/--model[=\s]+(\S+)/);
|
|
1831
1851
|
if (modelMatch) {
|
|
1832
1852
|
agent.model = modelMatch[1];
|
|
1833
1853
|
}
|
|
@@ -1929,12 +1949,15 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1929
1949
|
const broadcastData = async () => {
|
|
1930
1950
|
try {
|
|
1931
1951
|
const data = await getAllData();
|
|
1932
|
-
const
|
|
1952
|
+
const rawPayload = JSON.stringify(data);
|
|
1933
1953
|
// Guard against empty/invalid payloads
|
|
1934
|
-
if (!
|
|
1954
|
+
if (!rawPayload || rawPayload.length === 0) {
|
|
1935
1955
|
console.warn('[dashboard] Skipping broadcast - empty payload');
|
|
1936
1956
|
return;
|
|
1937
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 });
|
|
1938
1961
|
wss.clients.forEach(client => {
|
|
1939
1962
|
// Skip clients that are still being initialized by the connection handler
|
|
1940
1963
|
if (initializingClients.has(client)) {
|
|
@@ -2044,6 +2067,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2044
2067
|
ws.on('pong', () => {
|
|
2045
2068
|
mainClientAlive.set(ws, true);
|
|
2046
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
|
+
}
|
|
2047
2074
|
// Mark as initializing to prevent broadcastData from sending before we do
|
|
2048
2075
|
initializingClients.add(ws);
|
|
2049
2076
|
try {
|
|
@@ -2070,6 +2097,39 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2070
2097
|
// Now allow broadcastData to send to this client
|
|
2071
2098
|
initializingClients.delete(ws);
|
|
2072
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
|
+
});
|
|
2073
2133
|
ws.on('error', (err) => {
|
|
2074
2134
|
console.error('[dashboard] WebSocket client error:', err);
|
|
2075
2135
|
});
|
|
@@ -2105,9 +2165,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2105
2165
|
});
|
|
2106
2166
|
// Track alive status for ping/pong keepalive on log connections
|
|
2107
2167
|
const logClientAlive = new WeakMap();
|
|
2108
|
-
// Ping interval for log WebSocket connections (
|
|
2109
|
-
//
|
|
2110
|
-
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;
|
|
2111
2171
|
const logPingInterval = setInterval(() => {
|
|
2112
2172
|
wssLogs.clients.forEach((ws) => {
|
|
2113
2173
|
if (logClientAlive.get(ws) === false) {
|
|
@@ -2135,6 +2195,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2135
2195
|
ws.on('pong', () => {
|
|
2136
2196
|
logClientAlive.set(ws, true);
|
|
2137
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
|
+
}
|
|
2138
2202
|
// Helper to check if agent is daemon-connected (from agents.json)
|
|
2139
2203
|
const isDaemonConnected = (agentName) => {
|
|
2140
2204
|
const agentsPath = path.join(teamDir, 'agents.json');
|
|
@@ -2214,6 +2278,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2214
2278
|
data: newContent,
|
|
2215
2279
|
timestamp: new Date().toISOString(),
|
|
2216
2280
|
});
|
|
2281
|
+
// Push into per-agent log buffer for replay on reconnect
|
|
2282
|
+
getAgentLogBuffer(agentName).push('output', payload);
|
|
2217
2283
|
for (const client of clients) {
|
|
2218
2284
|
if (client.readyState === WebSocket.OPEN) {
|
|
2219
2285
|
client.send(payload);
|
|
@@ -2388,6 +2454,29 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2388
2454
|
}));
|
|
2389
2455
|
}
|
|
2390
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
|
+
}
|
|
2391
2480
|
}
|
|
2392
2481
|
catch (err) {
|
|
2393
2482
|
console.error('[dashboard] Invalid logs WebSocket message:', err);
|
|
@@ -2408,6 +2497,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2408
2497
|
watcher.close();
|
|
2409
2498
|
fileWatchers.delete(agentName);
|
|
2410
2499
|
fileLastSize.delete(agentName);
|
|
2500
|
+
agentLogBuffers.delete(agentName);
|
|
2411
2501
|
console.log(`[dashboard] Stopped watching log file for: ${agentName}`);
|
|
2412
2502
|
}
|
|
2413
2503
|
}
|
|
@@ -2461,12 +2551,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2461
2551
|
agentHashes.delete(oldest);
|
|
2462
2552
|
}
|
|
2463
2553
|
}
|
|
2464
|
-
const
|
|
2554
|
+
const logPayload = {
|
|
2465
2555
|
type: 'output',
|
|
2466
2556
|
agent: agentName,
|
|
2467
2557
|
data: output,
|
|
2468
2558
|
timestamp: new Date().toISOString(),
|
|
2469
|
-
}
|
|
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);
|
|
2470
2564
|
for (const client of clients) {
|
|
2471
2565
|
if (client.readyState === WebSocket.OPEN) {
|
|
2472
2566
|
client.send(payload);
|
|
@@ -2488,7 +2582,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2488
2582
|
// Helper to broadcast channel messages to all connected clients
|
|
2489
2583
|
// Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
|
|
2490
2584
|
const broadcastChannelMessage = (message) => {
|
|
2491
|
-
|
|
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 });
|
|
2492
2589
|
// Broadcast to main WebSocket clients (local mode)
|
|
2493
2590
|
wss.clients.forEach((client) => {
|
|
2494
2591
|
if (client.readyState === WebSocket.OPEN) {
|
|
@@ -2506,7 +2603,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2506
2603
|
// This enables agent replies to appear in the dashboard UI
|
|
2507
2604
|
// Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
|
|
2508
2605
|
const broadcastDirectMessage = (message) => {
|
|
2509
|
-
|
|
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 });
|
|
2510
2610
|
// Broadcast to main WebSocket clients (local mode)
|
|
2511
2611
|
const mainClients = Array.from(wss.clients).filter(c => c.readyState === WebSocket.OPEN);
|
|
2512
2612
|
debug(`[dashboard] Broadcasting direct_message to ${mainClients.length} main clients`);
|