rterm-backend 3.1.4 → 3.1.5
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/bin/gybackend.cjs
CHANGED
|
@@ -285128,24 +285128,41 @@ var init_natsEventBus = __esm({
|
|
|
285128
285128
|
if (a.tlsCa) tls.ca = a.tlsCa;
|
|
285129
285129
|
return tls;
|
|
285130
285130
|
}
|
|
285131
|
-
/** Connect to NATS (idempotent). Wires lifecycle event handlers.
|
|
285131
|
+
/** Connect to NATS (idempotent). Wires lifecycle event handlers.
|
|
285132
|
+
* Concurrent callers share one in-flight attempt; a failed attempt clears the
|
|
285133
|
+
* slot so the next call retries (no permanent half-connected state). */
|
|
285134
|
+
connectPromise = null;
|
|
285132
285135
|
async connect() {
|
|
285133
|
-
if (this.conn) return;
|
|
285134
|
-
|
|
285135
|
-
|
|
285136
|
-
|
|
285137
|
-
|
|
285138
|
-
|
|
285139
|
-
|
|
285140
|
-
|
|
285141
|
-
|
|
285142
|
-
|
|
285143
|
-
|
|
285144
|
-
|
|
285145
|
-
|
|
285146
|
-
|
|
285147
|
-
|
|
285148
|
-
|
|
285136
|
+
if (this.conn && !this.conn.isClosed()) return;
|
|
285137
|
+
if (this.conn) {
|
|
285138
|
+
this.conn = null;
|
|
285139
|
+
}
|
|
285140
|
+
if (this.connectPromise) return this.connectPromise;
|
|
285141
|
+
const attempt = (async () => {
|
|
285142
|
+
const connectFn = this.opts.connectFn ?? ((o) => (0, import_transport_node.connect)(o));
|
|
285143
|
+
const authenticator = this.buildAuthenticator();
|
|
285144
|
+
const tls = this.buildTls();
|
|
285145
|
+
const copts = {
|
|
285146
|
+
servers: this.opts.servers,
|
|
285147
|
+
name: this.opts.name ?? "rterm-backend",
|
|
285148
|
+
...authenticator ? { authenticator } : {},
|
|
285149
|
+
...tls ? { tls } : {},
|
|
285150
|
+
...this.opts.maxReconnectAttempts !== void 0 ? { maxReconnectAttempts: this.opts.maxReconnectAttempts } : {},
|
|
285151
|
+
...this.opts.reconnectTimeWait !== void 0 ? { reconnectTimeWait: this.opts.reconnectTimeWait } : {},
|
|
285152
|
+
...this.opts.timeout !== void 0 ? { timeout: this.opts.timeout } : {}
|
|
285153
|
+
};
|
|
285154
|
+
const c = await connectFn(copts);
|
|
285155
|
+
if (c.isClosed()) throw new Error("NATS connection closed immediately after connect");
|
|
285156
|
+
this.conn = c;
|
|
285157
|
+
this.wireStatusHandlers(c);
|
|
285158
|
+
this.log(`[nats] connected to ${Array.isArray(this.opts.servers) ? this.opts.servers.join(",") : this.opts.servers}`);
|
|
285159
|
+
})();
|
|
285160
|
+
this.connectPromise = attempt;
|
|
285161
|
+
try {
|
|
285162
|
+
await attempt;
|
|
285163
|
+
} finally {
|
|
285164
|
+
this.connectPromise = null;
|
|
285165
|
+
}
|
|
285149
285166
|
}
|
|
285150
285167
|
/** Attach reconnect/disconnect/error/lame-duck handlers (best-effort). */
|
|
285151
285168
|
wireStatusHandlers(conn) {
|
|
@@ -289757,6 +289774,8 @@ function constructPort(Ctor, path32, opts) {
|
|
|
289757
289774
|
}
|
|
289758
289775
|
var SerialBackend = class {
|
|
289759
289776
|
instances = /* @__PURE__ */ new Map();
|
|
289777
|
+
/** ids whose port errored (removed from instances but remembered as 'failed'). */
|
|
289778
|
+
failedIds = /* @__PURE__ */ new Set();
|
|
289760
289779
|
/** For tests: inject a fake serialport constructor/factory. */
|
|
289761
289780
|
static setSerialModuleForTest(mod) {
|
|
289762
289781
|
injectedSerial = mod;
|
|
@@ -289811,7 +289830,10 @@ var SerialBackend = class {
|
|
|
289811
289830
|
port.on("error", (err) => {
|
|
289812
289831
|
instance.dataCallback?.(`\x1B[31m\u2718 Serial error: ${err.message}\x1B[0m\r
|
|
289813
289832
|
`);
|
|
289833
|
+
instance.failed = true;
|
|
289814
289834
|
instance.exitCallback?.(-1);
|
|
289835
|
+
this.instances.delete(ptyId);
|
|
289836
|
+
this.failedIds.add(ptyId);
|
|
289815
289837
|
});
|
|
289816
289838
|
return Promise.resolve(ptyId);
|
|
289817
289839
|
}
|
|
@@ -289822,6 +289844,7 @@ var SerialBackend = class {
|
|
|
289822
289844
|
resize(_ptyId, _cols, _rows) {
|
|
289823
289845
|
}
|
|
289824
289846
|
kill(ptyId) {
|
|
289847
|
+
this.failedIds.delete(ptyId);
|
|
289825
289848
|
const inst = this.instances.get(ptyId);
|
|
289826
289849
|
if (!inst) return;
|
|
289827
289850
|
this.instances.delete(ptyId);
|
|
@@ -289853,8 +289876,9 @@ var SerialBackend = class {
|
|
|
289853
289876
|
}
|
|
289854
289877
|
getInitializationState(ptyId) {
|
|
289855
289878
|
const inst = this.instances.get(ptyId);
|
|
289856
|
-
if (
|
|
289857
|
-
|
|
289879
|
+
if (inst) return inst.ready ? "ready" : void 0;
|
|
289880
|
+
if (this.failedIds.has(ptyId)) return "failed";
|
|
289881
|
+
return void 0;
|
|
289858
289882
|
}
|
|
289859
289883
|
// --- Serial-specific controls (v3.0.5) ---
|
|
289860
289884
|
/** Send a BREAK signal (Cisco password recovery / ROMMON). Default 500ms. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "Headless AI-native backend for RTerm / neuralOS — v3.1.
|
|
3
|
+
"version": "3.1.5",
|
|
4
|
+
"description": "Headless AI-native backend for RTerm / neuralOS — v3.1.5: latent connection-state bug fixes (NATS connect concurrency/DOA guard, synapse-bridge config-keyed connections, serial port error leak). 11 plugins. Transports (SSH/serial/local), SQLite, and NATS libs install automatically.",
|
|
5
5
|
"main": "bin/gybackend.cjs",
|
|
6
6
|
"bin": { "gybackend": "bin/gybackend.cjs" },
|
|
7
7
|
"license": "MIT",
|
|
@@ -75,20 +75,47 @@ function buildAuthenticator(t, auth) {
|
|
|
75
75
|
return undefined
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// Connection cache keyed by config fingerprint — a settings change (different
|
|
79
|
+
// server/auth/agentId) opens a NEW connection instead of reusing a stale one to
|
|
80
|
+
// the wrong server. A failed connect clears the slot so the next call retries.
|
|
81
|
+
const _conns = new Map() // key -> Promise<conn> | conn
|
|
82
|
+
function _configKey(cfg) {
|
|
83
|
+
const servers = Array.isArray(cfg.servers) ? cfg.servers.join(',') : cfg.servers
|
|
84
|
+
const authKeys = cfg.auth ? Object.keys(cfg.auth).sort().join(',') : ''
|
|
85
|
+
return `${servers}|${cfg.agentId}|${authKeys}`
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
async function connectMesh(ctx) {
|
|
80
|
-
if (_conn && !_conn.isClosed()) return _conn
|
|
81
89
|
const cfg = resolveConfig(ctx)
|
|
90
|
+
const key = _configKey(cfg)
|
|
91
|
+
const existing = _conns.get(key)
|
|
92
|
+
if (existing) {
|
|
93
|
+
const c = await existing
|
|
94
|
+
if (c && !c.isClosed()) return c
|
|
95
|
+
_conns.delete(key) // stale/closed — fall through and reconnect
|
|
96
|
+
}
|
|
82
97
|
const t = loadTransport()
|
|
83
98
|
const auth = buildAuthenticator(t, resolveAuth(ctx, cfg.auth))
|
|
84
99
|
const copts = { servers: cfg.servers, name: cfg.agentId, ...(auth ? { authenticator: auth } : {}) }
|
|
85
100
|
const connectFn = (typeof ctx.natsConnect === 'function') ? ctx.natsConnect : (o) => t.connect(o)
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
const p = (async () => {
|
|
102
|
+
try {
|
|
103
|
+
return await connectFn(copts)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
_conns.delete(key) // don't cache a failed attempt — allow retry
|
|
106
|
+
throw e
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
109
|
+
_conns.set(key, p)
|
|
110
|
+
return p
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
/** Test hook: inject a fake connection. */
|
|
91
|
-
export function __setConnForTest(c) {
|
|
113
|
+
/** Test hook: inject a fake connection for a given config (or clear all with null). */
|
|
114
|
+
export function __setConnForTest(c, cfg) {
|
|
115
|
+
if (c === null || c === undefined) { _conns.clear(); return }
|
|
116
|
+
const key = _configKey(cfg ?? resolveConfig({ settings: {} }))
|
|
117
|
+
_conns.set(key, Promise.resolve(c))
|
|
118
|
+
}
|
|
92
119
|
|
|
93
120
|
// ─── Synapse envelope ───────────────────────────────────────────────────────
|
|
94
121
|
|
|
@@ -161,6 +161,51 @@ test('synapse_mesh_event trigger matches only synapse-source events', () => {
|
|
|
161
161
|
assert(!t.match({}), 'rejects empty')
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
+
// ─── connection cache (config-keyed, the stale-connection bug fix) ──────────
|
|
165
|
+
|
|
166
|
+
test('connection is keyed by config — a settings change opens a NEW connection (no stale reuse)', async () => {
|
|
167
|
+
__setConnForTest(null)
|
|
168
|
+
const connA = fakeConn()
|
|
169
|
+
const connB = fakeConn()
|
|
170
|
+
const connsMade = []
|
|
171
|
+
// ctx whose natsConnect returns a different fake per call, tracking which config connected
|
|
172
|
+
const mkCtxMulti = (settings) => ({
|
|
173
|
+
settings: { synapse: settings },
|
|
174
|
+
natsConnect: async (copts) => { connsMade.push(copts); return connsMade.length === 1 ? connA : connB },
|
|
175
|
+
registerTool: () => {}, registerTrigger: () => {}, registerPanel: () => {}, log: () => {},
|
|
176
|
+
})
|
|
177
|
+
// connect with config A (server A)
|
|
178
|
+
const { discoverAgents: dA } = await import('./index.mjs')
|
|
179
|
+
connA._on('mesh.registry.discover', () => [])
|
|
180
|
+
await dA(mkCtxMulti({ url: 'nats://a:4222' }), {})
|
|
181
|
+
if (connsMade.length !== 1) throw new Error(`expected 1 connection for config A, got ${connsMade.length}`)
|
|
182
|
+
// same config A again — must REUSE (no new connection)
|
|
183
|
+
await dA(mkCtxMulti({ url: 'nats://a:4222' }), {})
|
|
184
|
+
if (connsMade.length !== 1) throw new Error(`expected reuse for same config A, got ${connsMade.length} connections`)
|
|
185
|
+
// config B (different server) — must open a NEW connection (the bug was reusing A's)
|
|
186
|
+
connB._on('mesh.registry.discover', () => [])
|
|
187
|
+
await dA(mkCtxMulti({ url: 'nats://b:4222' }), {})
|
|
188
|
+
if (connsMade.length !== 2) throw new Error(`expected a NEW connection for config B, got ${connsMade.length}`)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('a failed connect is not cached — the next call retries', async () => {
|
|
192
|
+
__setConnForTest(null)
|
|
193
|
+
const conn = fakeConn()
|
|
194
|
+
conn._on('mesh.registry.discover', () => [])
|
|
195
|
+
let attempts = 0
|
|
196
|
+
const ctx = {
|
|
197
|
+
settings: { synapse: { url: 'nats://a:4222' } },
|
|
198
|
+
natsConnect: async () => { attempts++; if (attempts === 1) throw new Error('down'); return conn },
|
|
199
|
+
registerTool: () => {}, registerTrigger: () => {}, registerPanel: () => {}, log: () => {},
|
|
200
|
+
}
|
|
201
|
+
const { discoverAgents } = await import('./index.mjs')
|
|
202
|
+
let threw = false
|
|
203
|
+
try { await discoverAgents(ctx, {}) } catch { threw = true }
|
|
204
|
+
if (!threw) throw new Error('expected first attempt to throw')
|
|
205
|
+
await discoverAgents(ctx, {}) // retry succeeds
|
|
206
|
+
if (attempts !== 2) throw new Error(`expected 2 attempts (fail + retry), got ${attempts}`)
|
|
207
|
+
})
|
|
208
|
+
|
|
164
209
|
// ─── runner ─────────────────────────────────────────────────────────────────
|
|
165
210
|
async function main() {
|
|
166
211
|
let pass = 0, fail = 0
|