@anusornneal/chat-relay 0.7.2 → 0.8.1

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.
@@ -0,0 +1,121 @@
1
+ const DEFAULT_QUEUE_TIMEOUT_MS = 15_000;
2
+ const DEFAULT_MAX_QUEUED = 64;
3
+
4
+ function boundedInteger(value, fallback, min, max) {
5
+ const number = Number(value);
6
+ if (!Number.isFinite(number)) return fallback;
7
+ return Math.min(Math.max(Math.trunc(number), min), max);
8
+ }
9
+
10
+ function queueError(code) {
11
+ const error = new Error(code);
12
+ error.code = code;
13
+ return error;
14
+ }
15
+
16
+ export class BoundedLane {
17
+ constructor(options = {}) {
18
+ this.name = String(options.name || "lane");
19
+ this.concurrency = boundedInteger(options.concurrency, 1, 1, 64);
20
+ this.maxQueued = boundedInteger(options.maxQueued, DEFAULT_MAX_QUEUED, 1, 512);
21
+ this.queueTimeoutMs = boundedInteger(options.queueTimeoutMs, DEFAULT_QUEUE_TIMEOUT_MS, 100, 120_000);
22
+ this.active = 0;
23
+ this.queue = [];
24
+ }
25
+
26
+ run(work) {
27
+ if (typeof work !== "function") return Promise.reject(queueError("invalid_work"));
28
+ return new Promise((resolve, reject) => {
29
+ const job = { work, resolve, reject, timer: null, enqueuedAt: Date.now() };
30
+ if (this.active < this.concurrency) {
31
+ this.#start(job);
32
+ return;
33
+ }
34
+ if (this.queue.length >= this.maxQueued) {
35
+ reject(queueError("queue_full"));
36
+ return;
37
+ }
38
+ job.timer = setTimeout(() => {
39
+ const index = this.queue.indexOf(job);
40
+ if (index < 0) return;
41
+ this.queue.splice(index, 1);
42
+ reject(queueError("queue_timeout"));
43
+ }, this.queueTimeoutMs);
44
+ this.queue.push(job);
45
+ });
46
+ }
47
+
48
+ snapshot() {
49
+ return {
50
+ concurrency: this.concurrency,
51
+ active: this.active,
52
+ queued: this.queue.length,
53
+ maxQueued: this.maxQueued,
54
+ queueTimeoutMs: this.queueTimeoutMs,
55
+ };
56
+ }
57
+
58
+ #start(job) {
59
+ if (job.timer) clearTimeout(job.timer);
60
+ this.active += 1;
61
+ const queueWaitMs = Math.max(0, Date.now() - job.enqueuedAt);
62
+ const metadata = {
63
+ lane: this.name,
64
+ queueWaitMs,
65
+ queueDepthAtStart: this.queue.length,
66
+ activeAtStart: this.active,
67
+ };
68
+ Promise.resolve()
69
+ .then(() => job.work(metadata))
70
+ .then(job.resolve, job.reject)
71
+ .finally(() => {
72
+ this.active -= 1;
73
+ this.#drain();
74
+ });
75
+ }
76
+
77
+ #drain() {
78
+ while (this.active < this.concurrency && this.queue.length > 0) {
79
+ this.#start(this.queue.shift());
80
+ }
81
+ }
82
+ }
83
+
84
+ export class CapabilityScheduler {
85
+ constructor(options = {}) {
86
+ const maxQueued = boundedInteger(options.maxQueued, DEFAULT_MAX_QUEUED, 1, 512);
87
+ const queueTimeoutMs = boundedInteger(options.queueTimeoutMs, DEFAULT_QUEUE_TIMEOUT_MS, 100, 120_000);
88
+ const lane = (name, concurrency) => new BoundedLane({ name, concurrency, maxQueued, queueTimeoutMs });
89
+
90
+ this.lanes = new Map([
91
+ ["agent", lane("agent", options.agentConcurrency ?? 4)],
92
+ ["filesystem", lane("filesystem", options.fileConcurrency ?? 8)],
93
+ ["process", lane("process", options.processConcurrency ?? 4)],
94
+ ["terminalExec", lane("terminalExec", options.terminalExecConcurrency ?? 4)],
95
+ ["terminalControl", lane("terminalControl", options.terminalControlConcurrency ?? 8)],
96
+ ["desktopRead", lane("desktopRead", options.desktopReadConcurrency ?? 2)],
97
+ ]);
98
+ }
99
+
100
+ run(action, work) {
101
+ const lane = this.#laneFor(action);
102
+ return lane ? lane.run(work) : Promise.resolve().then(work);
103
+ }
104
+
105
+ snapshot() {
106
+ return Object.fromEntries([...this.lanes.entries()].map(([name, lane]) => [name, lane.snapshot()]));
107
+ }
108
+
109
+ #laneFor(action) {
110
+ const value = String(action || "unknown");
111
+ if (value.startsWith("fs.")) return this.lanes.get("filesystem");
112
+ if (value.startsWith("process.")) return this.lanes.get("process");
113
+ if (value === "terminal.exec") return this.lanes.get("terminalExec");
114
+ if (value.startsWith("terminal.")) return this.lanes.get("terminalControl");
115
+ if (value === "desktop.screenshot") return this.lanes.get("desktopRead");
116
+ if (value.startsWith("desktop.")) return null;
117
+ return this.lanes.get("agent");
118
+ }
119
+ }
120
+
121
+ export { boundedInteger };
@@ -0,0 +1,141 @@
1
+ function boundedInteger(value, fallback, min, max) {
2
+ const number = Number(value);
3
+ if (!Number.isFinite(number)) return fallback;
4
+ return Math.min(Math.max(Math.trunc(number), min), max);
5
+ }
6
+
7
+ function isoTime(value) {
8
+ const number = Number(value);
9
+ return new Date(Number.isFinite(number) ? number : Date.now()).toISOString();
10
+ }
11
+
12
+ function cleanReason(value) {
13
+ const reason = String(value || "").trim();
14
+ return reason ? reason.slice(0, 160) : null;
15
+ }
16
+
17
+ export function computeReconnectDelay(attempt, options = {}) {
18
+ const baseMs = boundedInteger(options.baseMs, 2000, 100, 60_000);
19
+ const maxMs = boundedInteger(options.maxMs, 30_000, baseMs, 120_000);
20
+ const jitterRatio = Math.min(Math.max(Number(options.jitterRatio ?? 0.2), 0), 0.5);
21
+ const random = typeof options.random === "function" ? options.random : Math.random;
22
+ const exponent = Math.min(Math.max(Math.trunc(Number(attempt) || 1) - 1, 0), 10);
23
+ const raw = Math.min(maxMs, baseMs * (2 ** exponent));
24
+ const spread = raw * jitterRatio;
25
+ const sample = Math.min(Math.max(Number(random()) || 0, 0), 1);
26
+ return Math.max(100, Math.round(raw - spread + (spread * 2 * sample)));
27
+ }
28
+
29
+ export class AgentConnectionState {
30
+ constructor(options = {}) {
31
+ this.baseReconnectMs = boundedInteger(options.baseReconnectMs, 2000, 100, 60_000);
32
+ this.maxReconnectMs = boundedInteger(options.maxReconnectMs, 30_000, this.baseReconnectMs, 120_000);
33
+ this.heartbeatMs = boundedInteger(options.heartbeatMs, 15_000, 5_000, 60_000);
34
+ this.processStartedAt = new Date().toISOString();
35
+ this.state = "starting";
36
+ this.connectedAt = null;
37
+ this.disconnectedAt = null;
38
+ this.lastDisconnectedAt = null;
39
+ this.lastHeartbeatAt = null;
40
+ this.lastCloseCode = null;
41
+ this.lastDisconnectReason = null;
42
+ this.lastConnectionDurationMs = null;
43
+ this.lastSocketError = null;
44
+ this.reconnectAttempt = 0;
45
+ this.reconnectCount = 0;
46
+ this.nextReconnectAt = null;
47
+ this.nextReconnectDelayMs = null;
48
+ }
49
+
50
+ markConnecting(now = Date.now()) {
51
+ this.state = "connecting";
52
+ this.nextReconnectAt = null;
53
+ this.nextReconnectDelayMs = null;
54
+ this.connectingAt = isoTime(now);
55
+ }
56
+
57
+ markConnected(now = Date.now()) {
58
+ this.state = "connected";
59
+ this.connectedAt = isoTime(now);
60
+ this.disconnectedAt = null;
61
+ this.reconnectAttempt = 0;
62
+ this.nextReconnectAt = null;
63
+ this.nextReconnectDelayMs = null;
64
+ }
65
+
66
+ markHeartbeat(now = Date.now()) {
67
+ this.lastHeartbeatAt = isoTime(now);
68
+ }
69
+
70
+ markDisconnected(code, reason, now = Date.now()) {
71
+ this.state = "waiting";
72
+ const disconnectedAt = isoTime(now);
73
+ const connectedAtMs = this.connectedAt ? Date.parse(this.connectedAt) : NaN;
74
+ this.disconnectedAt = disconnectedAt;
75
+ this.lastDisconnectedAt = disconnectedAt;
76
+ this.lastCloseCode = Number.isFinite(Number(code)) ? Number(code) : null;
77
+ this.lastDisconnectReason = cleanReason(reason);
78
+ this.lastConnectionDurationMs = Number.isFinite(connectedAtMs)
79
+ ? Math.max(0, Math.trunc(Number(now) - connectedAtMs))
80
+ : null;
81
+ this.reconnectAttempt += 1;
82
+ this.reconnectCount += 1;
83
+ }
84
+
85
+ markSocketError(reason) {
86
+ this.lastSocketError = cleanReason(reason);
87
+ }
88
+
89
+ scheduleReconnect(delayMs, now = Date.now()) {
90
+ const delay = boundedInteger(delayMs, this.baseReconnectMs, 100, this.maxReconnectMs);
91
+ this.nextReconnectDelayMs = delay;
92
+ this.nextReconnectAt = isoTime(Number(now) + delay);
93
+ }
94
+
95
+ nextDelay(random = Math.random) {
96
+ return computeReconnectDelay(this.reconnectAttempt, {
97
+ baseMs: this.baseReconnectMs,
98
+ maxMs: this.maxReconnectMs,
99
+ random,
100
+ });
101
+ }
102
+
103
+ markReauthorization(reason = "credential_revoked", now = Date.now()) {
104
+ this.state = "reauthorization-required";
105
+ this.disconnectedAt = isoTime(now);
106
+ this.lastDisconnectReason = cleanReason(reason);
107
+ this.nextReconnectAt = null;
108
+ this.nextReconnectDelayMs = null;
109
+ }
110
+
111
+ markStopping(reason = "shutdown", now = Date.now()) {
112
+ this.state = "stopping";
113
+ this.disconnectedAt = isoTime(now);
114
+ this.lastDisconnectReason = cleanReason(reason);
115
+ this.nextReconnectAt = null;
116
+ this.nextReconnectDelayMs = null;
117
+ }
118
+
119
+ snapshot() {
120
+ return {
121
+ state: this.state,
122
+ processStartedAt: this.processStartedAt,
123
+ connectingAt: this.connectingAt ?? null,
124
+ connectedAt: this.connectedAt,
125
+ disconnectedAt: this.disconnectedAt,
126
+ lastDisconnectedAt: this.lastDisconnectedAt,
127
+ lastHeartbeatAt: this.lastHeartbeatAt,
128
+ lastCloseCode: this.lastCloseCode,
129
+ lastDisconnectReason: this.lastDisconnectReason,
130
+ lastConnectionDurationMs: this.lastConnectionDurationMs,
131
+ lastSocketError: this.lastSocketError,
132
+ reconnectAttempt: this.reconnectAttempt,
133
+ reconnectCount: this.reconnectCount,
134
+ nextReconnectAt: this.nextReconnectAt,
135
+ nextReconnectDelayMs: this.nextReconnectDelayMs,
136
+ baseReconnectMs: this.baseReconnectMs,
137
+ maxReconnectMs: this.maxReconnectMs,
138
+ heartbeatMs: this.heartbeatMs,
139
+ };
140
+ }
141
+ }