rterm-backend 3.0.3 → 3.0.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.
Files changed (2) hide show
  1. package/bin/gybackend.cjs +563 -28
  2. package/package.json +2 -2
package/bin/gybackend.cjs CHANGED
@@ -284093,6 +284093,37 @@ var WinRMTransport = class {
284093
284093
  await this.deleteShell(shellId);
284094
284094
  }
284095
284095
  }
284096
+ /**
284097
+ * Run a command on an EXISTING (persistent) shell, streaming output chunks to
284098
+ * `onChunk` as they arrive instead of buffering everything. The shell is left
284099
+ * open for the next command (persistent runspace). The caller owns the shell's
284100
+ * lifecycle (createShell/deleteShell) and must serialize commands per shell.
284101
+ */
284102
+ async runCommandOnShell(shellId, command, opts) {
284103
+ const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
284104
+ const commandId = await this.sendCommand(shellId, command);
284105
+ let stdout = "";
284106
+ let stderr = "";
284107
+ let exitCode = 0;
284108
+ for (; ; ) {
284109
+ if (opts?.signal?.aborted) throw new Error("AbortError");
284110
+ if (Date.now() > deadline) {
284111
+ throw new Error(`WinRM command timed out after ${opts?.timeoutMs ?? 12e4}ms`);
284112
+ }
284113
+ const r = await this.receive(shellId, commandId);
284114
+ stdout += r.stdout;
284115
+ stderr += r.stderr;
284116
+ if (opts?.onChunk) {
284117
+ if (r.stdout) opts.onChunk("stdout", r.stdout);
284118
+ if (r.stderr) opts.onChunk("stderr", r.stderr);
284119
+ }
284120
+ if (r.done) {
284121
+ exitCode = r.exitCode ?? 0;
284122
+ break;
284123
+ }
284124
+ }
284125
+ return { stdout, stderr, exitCode };
284126
+ }
284096
284127
  /** Lightweight connectivity probe: create then immediately delete a shell. */
284097
284128
  async ping() {
284098
284129
  const shellId = await this.createShell();
@@ -284123,7 +284154,7 @@ var WinRMBackend = class {
284123
284154
  const cfg = config2;
284124
284155
  const ptyId = `winrm-${(0, import_node_crypto2.randomUUID)()}`;
284125
284156
  const transport = this.buildTransport(cfg);
284126
- const instance = { config: cfg, transport, ready: false, failed: false };
284157
+ const instance = { config: cfg, transport, ready: false, failed: false, commandQueue: Promise.resolve() };
284127
284158
  this.instances.set(ptyId, instance);
284128
284159
  void this.probe(instance).then((ok) => {
284129
284160
  if (ok) {
@@ -284166,7 +284197,9 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
284166
284197
  timeoutMs: 3e4
284167
284198
  });
284168
284199
  }
284169
- /** Direct command execution — the path TerminalService uses for winrm tabs. */
284200
+ /** Direct command execution — the path TerminalService uses for winrm tabs.
284201
+ * Uses a persistent runspace (reused across commands) + streams output live,
284202
+ * and tracks the working directory so `cd` persists between commands. */
284170
284203
  async executeCommand(ptyId, command, options) {
284171
284204
  const instance = this.instances.get(ptyId);
284172
284205
  if (!instance) {
@@ -284179,16 +284212,60 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
284179
284212
  if (!waited) {
284180
284213
  throw new Error("WinRM session is still initializing; try again shortly.");
284181
284214
  }
284215
+ const run = instance.commandQueue.then(
284216
+ () => this.executeOnPersistentShell(instance, command, options)
284217
+ );
284218
+ instance.commandQueue = run.catch(() => {
284219
+ });
284220
+ return run;
284221
+ }
284222
+ /** Lazily create (or recreate) the persistent runspace. */
284223
+ async ensurePersistentShell(instance) {
284224
+ if (instance.persistentShellId) return instance.persistentShellId;
284225
+ const shellId = await instance.transport.createShell();
284226
+ instance.persistentShellId = shellId;
284227
+ try {
284228
+ const r = await instance.transport.runCommandOnShell(shellId, "cd", { timeoutMs: 1e4 });
284229
+ const cwd = r.stdout.trim();
284230
+ if (cwd) instance.cwd = cwd;
284231
+ } catch {
284232
+ }
284233
+ return shellId;
284234
+ }
284235
+ async executeOnPersistentShell(instance, command, options) {
284182
284236
  instance.dataCallback?.(`\r
284183
284237
  \x1B[36m\u276F ${command}\x1B[0m\r
284184
284238
  `);
284185
- const result = await instance.transport.runCommand(command, {
284186
- timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
284187
- signal: options?.signal
284188
- });
284189
- if (result.stdout) instance.dataCallback?.(result.stdout);
284190
- if (result.stderr) {
284191
- instance.dataCallback?.(`\x1B[33m${result.stderr}\x1B[0m`);
284239
+ let result;
284240
+ const cwdPrefix = instance.cwd ? `cd /d ${instance.cwd} & ` : "";
284241
+ const isCd = /^\s*(cd|chdir)\s+/i.test(command);
284242
+ try {
284243
+ const shellId = await this.ensurePersistentShell(instance);
284244
+ result = await instance.transport.runCommandOnShell(shellId, cwdPrefix + command, {
284245
+ timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
284246
+ signal: options?.signal,
284247
+ onChunk: (stream, text) => {
284248
+ if (text) instance.dataCallback?.(stream === "stderr" ? `\x1B[33m${text}\x1B[0m` : text);
284249
+ }
284250
+ });
284251
+ if (isCd && result.exitCode === 0) {
284252
+ const target = command.replace(/^\s*(cd|chdir)\s+\/?d?\s*/i, "").replace(/"/g, "").trim();
284253
+ instance.cwd = this.resolveWinCwd(instance.cwd, target);
284254
+ } else if (result.exitCode === 0) {
284255
+ const probe = await instance.transport.runCommandOnShell(shellId, `${cwdPrefix}cd`, { timeoutMs: 1e4 }).catch(() => null);
284256
+ const probeCwd = probe?.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => /^[A-Za-z]:\\/.test(l)).pop();
284257
+ if (probeCwd) instance.cwd = probeCwd;
284258
+ }
284259
+ } catch (error40) {
284260
+ instance.persistentShellId = void 0;
284261
+ const shellId = await this.ensurePersistentShell(instance);
284262
+ result = await instance.transport.runCommandOnShell(shellId, cwdPrefix + command, {
284263
+ timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
284264
+ signal: options?.signal,
284265
+ onChunk: (stream, text) => {
284266
+ if (text) instance.dataCallback?.(stream === "stderr" ? `\x1B[33m${text}\x1B[0m` : text);
284267
+ }
284268
+ });
284192
284269
  }
284193
284270
  instance.dataCallback?.(
284194
284271
  `\r
@@ -284197,6 +284274,22 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
284197
284274
  );
284198
284275
  return result;
284199
284276
  }
284277
+ /** Resolve a `cd` target (absolute or relative) against the tracked cwd. */
284278
+ resolveWinCwd(currentCwd, target) {
284279
+ if (!target) return currentCwd ?? "";
284280
+ if (/^[A-Za-z]:[\\/]/.test(target)) {
284281
+ return target.replace(/\//g, "\\").replace(/\\+$/, "");
284282
+ }
284283
+ if (/^[A-Za-z]:$/.test(target)) return `${target}\\`;
284284
+ const base = (currentCwd ?? "C:\\").replace(/\\+$/, "");
284285
+ const parts = base.split("\\").filter(Boolean);
284286
+ for (const seg of target.replace(/\//g, "\\").split("\\")) {
284287
+ if (seg === "" || seg === ".") continue;
284288
+ if (seg === "..") parts.pop();
284289
+ else parts.push(seg);
284290
+ }
284291
+ return parts.join("\\");
284292
+ }
284200
284293
  async waitForReady(instance, timeoutMs) {
284201
284294
  if (instance.ready) return true;
284202
284295
  if (instance.failed) return false;
@@ -284217,6 +284310,10 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
284217
284310
  const instance = this.instances.get(ptyId);
284218
284311
  if (!instance) return;
284219
284312
  this.instances.delete(ptyId);
284313
+ if (instance.persistentShellId) {
284314
+ void instance.transport.deleteShell(instance.persistentShellId);
284315
+ instance.persistentShellId = void 0;
284316
+ }
284220
284317
  instance.exitCallback?.(0);
284221
284318
  }
284222
284319
  onData(ptyId, callback) {
@@ -284227,8 +284324,8 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
284227
284324
  const instance = this.instances.get(ptyId);
284228
284325
  if (instance) instance.exitCallback = callback;
284229
284326
  }
284230
- getCwd(_ptyId) {
284231
- return void 0;
284327
+ getCwd(ptyId) {
284328
+ return this.instances.get(ptyId)?.cwd;
284232
284329
  }
284233
284330
  getHomeDir(_ptyId) {
284234
284331
  return Promise.resolve(void 0);
@@ -284368,6 +284465,35 @@ var SerialBackend = class {
284368
284465
  if (!inst) return void 0;
284369
284466
  return inst.ready ? "ready" : void 0;
284370
284467
  }
284468
+ // --- Serial-specific controls (v3.0.5) ---
284469
+ /** Send a BREAK signal (Cisco password recovery / ROMMON). Default 500ms. */
284470
+ sendBreak(ptyId, durationMs = 500) {
284471
+ const inst = this.instances.get(ptyId);
284472
+ if (!inst || typeof inst.port.break !== "function") return false;
284473
+ try {
284474
+ inst.port.break({ duration: durationMs }, (err) => {
284475
+ if (err) inst.dataCallback?.(`\x1B[31m\u2718 Break failed: ${err.message}\x1B[0m\r
284476
+ `);
284477
+ });
284478
+ return true;
284479
+ } catch {
284480
+ return false;
284481
+ }
284482
+ }
284483
+ /** Set modem control lines (DTR/RTS/CTS). */
284484
+ setControlLines(ptyId, lines) {
284485
+ const inst = this.instances.get(ptyId);
284486
+ if (!inst || typeof inst.port.set !== "function") return false;
284487
+ try {
284488
+ inst.port.set(lines, (err) => {
284489
+ if (err) inst.dataCallback?.(`\x1B[31m\u2718 set() failed: ${err.message}\x1B[0m\r
284490
+ `);
284491
+ });
284492
+ return true;
284493
+ } catch {
284494
+ return false;
284495
+ }
284496
+ }
284371
284497
  };
284372
284498
 
284373
284499
  // ../../packages/backend/src/services/ShellUtility.ts
@@ -284376,6 +284502,190 @@ function escapeShellPathList(paths) {
284376
284502
  return escaped.join(" ") + (escaped.length ? " " : "");
284377
284503
  }
284378
284504
 
284505
+ // ../../packages/backend/src/services/terminal/autoReconnect.ts
284506
+ var DEFAULT_BASE = 1e3;
284507
+ var DEFAULT_MAX = 6e4;
284508
+ var DEFAULT_JITTER = 0.2;
284509
+ var AutoReconnect = class {
284510
+ baseDelayMs;
284511
+ maxDelayMs;
284512
+ maxAttempts;
284513
+ jitterRatio;
284514
+ random;
284515
+ setTimeoutFn;
284516
+ clearTimeoutFn;
284517
+ /** terminalId → pending timer handle. */
284518
+ timers = /* @__PURE__ */ new Map();
284519
+ /** terminalId → attempts fired so far. */
284520
+ attempts = /* @__PURE__ */ new Map();
284521
+ /** terminalId → delay used for the pending attempt. */
284522
+ pendingDelay = /* @__PURE__ */ new Map();
284523
+ constructor(opts = {}) {
284524
+ this.baseDelayMs = Math.max(1, opts.baseDelayMs ?? DEFAULT_BASE);
284525
+ this.maxDelayMs = Math.max(this.baseDelayMs, opts.maxDelayMs ?? DEFAULT_MAX);
284526
+ this.maxAttempts = Math.max(1, opts.maxAttempts ?? Number.POSITIVE_INFINITY);
284527
+ this.jitterRatio = Math.min(1, Math.max(0, opts.jitterRatio ?? DEFAULT_JITTER));
284528
+ this.random = opts.random ?? (() => Math.random());
284529
+ this.setTimeoutFn = opts.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
284530
+ this.clearTimeoutFn = opts.clearTimeoutFn ?? ((h) => clearTimeout(h));
284531
+ }
284532
+ /** Compute the delay for attempt N (1-based): base * 2^(n-1), capped, ±jitter. */
284533
+ delayForAttempt(attempt) {
284534
+ const expo = this.baseDelayMs * Math.pow(2, Math.max(0, attempt - 1));
284535
+ const capped = Math.min(this.maxDelayMs, expo);
284536
+ const jitter = capped * this.jitterRatio * (this.random() * 2 - 1);
284537
+ return Math.max(1, Math.round(capped + jitter));
284538
+ }
284539
+ /** Whether a schedule is pending for this terminal. */
284540
+ isScheduled(terminalId) {
284541
+ return this.timers.has(terminalId);
284542
+ }
284543
+ /** Attempts fired so far for this terminal. */
284544
+ attemptsFor(terminalId) {
284545
+ return this.attempts.get(terminalId) ?? 0;
284546
+ }
284547
+ /** Snapshot the schedule state for a terminal (undefined if not scheduled). */
284548
+ state(terminalId) {
284549
+ if (!this.timers.has(terminalId)) return void 0;
284550
+ const fired = this.attempts.get(terminalId) ?? 0;
284551
+ return {
284552
+ terminalId,
284553
+ nextAttempt: fired + 1,
284554
+ nextDelayMs: this.pendingDelay.get(terminalId) ?? 0,
284555
+ attemptsFired: fired,
284556
+ active: true
284557
+ };
284558
+ }
284559
+ /**
284560
+ * Schedule the next reconnect attempt for a terminal. `onAttempt` is called
284561
+ * when the timer fires (it should perform the reconnect); `onGiveUp` is
284562
+ * called when maxAttempts is reached. Returns the schedule state, or
284563
+ * undefined if attempts are already exhausted.
284564
+ */
284565
+ schedule(terminalId, onAttempt, onGiveUp) {
284566
+ this.cancel(terminalId);
284567
+ const fired = this.attempts.get(terminalId) ?? 0;
284568
+ const nextAttempt = fired + 1;
284569
+ if (nextAttempt > this.maxAttempts) {
284570
+ onGiveUp?.(fired);
284571
+ return void 0;
284572
+ }
284573
+ const delay = this.delayForAttempt(nextAttempt);
284574
+ this.pendingDelay.set(terminalId, delay);
284575
+ const handle = this.setTimeoutFn(() => {
284576
+ this.timers.delete(terminalId);
284577
+ this.pendingDelay.delete(terminalId);
284578
+ const now = (this.attempts.get(terminalId) ?? 0) + 1;
284579
+ this.attempts.set(terminalId, now);
284580
+ onAttempt(now);
284581
+ }, delay);
284582
+ this.timers.set(terminalId, handle);
284583
+ return {
284584
+ terminalId,
284585
+ nextAttempt,
284586
+ nextDelayMs: delay,
284587
+ attemptsFired: fired,
284588
+ active: true
284589
+ };
284590
+ }
284591
+ /** Cancel a pending schedule (does NOT reset the attempt counter). */
284592
+ cancel(terminalId) {
284593
+ const handle = this.timers.get(terminalId);
284594
+ if (handle !== void 0) {
284595
+ this.clearTimeoutFn(handle);
284596
+ this.timers.delete(terminalId);
284597
+ }
284598
+ this.pendingDelay.delete(terminalId);
284599
+ return handle !== void 0;
284600
+ }
284601
+ /** Reset a terminal's attempt counter (call after a successful reconnect). */
284602
+ reset(terminalId) {
284603
+ this.attempts.delete(terminalId);
284604
+ this.pendingDelay.delete(terminalId);
284605
+ }
284606
+ /** Cancel + reset (call on manual kill / user reconnect / tab close). */
284607
+ clear(terminalId) {
284608
+ this.cancel(terminalId);
284609
+ this.reset(terminalId);
284610
+ }
284611
+ /** Cancel every pending schedule (shutdown). */
284612
+ clearAll() {
284613
+ for (const id of Array.from(this.timers.keys())) this.cancel(id);
284614
+ this.attempts.clear();
284615
+ this.pendingDelay.clear();
284616
+ }
284617
+ };
284618
+
284619
+ // ../../packages/backend/src/services/terminal/chunkedRingBuffer.ts
284620
+ var DEFAULT_MAX2 = 2e5;
284621
+ var DEFAULT_CHUNK = 16384;
284622
+ var ChunkedRingBuffer = class {
284623
+ chunks = [];
284624
+ maxSize;
284625
+ chunkSize;
284626
+ /** total chars currently retained. */
284627
+ retained = 0;
284628
+ /** total chars ever appended (monotonic; matches the old `offset`). */
284629
+ offset = 0;
284630
+ constructor(opts = {}) {
284631
+ this.maxSize = Math.max(1, opts.maxSize ?? DEFAULT_MAX2);
284632
+ this.chunkSize = Math.max(256, opts.chunkSize ?? DEFAULT_CHUNK);
284633
+ }
284634
+ /** Append data; drops whole old chunks if over maxSize. */
284635
+ append(data) {
284636
+ if (!data) return;
284637
+ this.offset += data.length;
284638
+ let rest = data;
284639
+ while (rest.length > 0) {
284640
+ const last = this.chunks[this.chunks.length - 1];
284641
+ if (last !== void 0 && last.length < this.chunkSize) {
284642
+ const take = Math.min(this.chunkSize - last.length, rest.length);
284643
+ this.chunks[this.chunks.length - 1] = last + rest.slice(0, take);
284644
+ this.retained += take;
284645
+ rest = rest.slice(take);
284646
+ } else {
284647
+ const take = Math.min(this.chunkSize, rest.length);
284648
+ this.chunks.push(rest.slice(0, take));
284649
+ this.retained += take;
284650
+ rest = rest.slice(take);
284651
+ }
284652
+ }
284653
+ this.evict();
284654
+ }
284655
+ /** Drop oldest whole chunks until retained <= maxSize. */
284656
+ evict() {
284657
+ while (this.retained > this.maxSize && this.chunks.length > 0) {
284658
+ const head = this.chunks[0];
284659
+ if (this.retained - head.length >= this.maxSize) {
284660
+ this.chunks.shift();
284661
+ this.retained -= head.length;
284662
+ } else {
284663
+ const trim = this.retained - this.maxSize;
284664
+ this.chunks[0] = head.slice(trim);
284665
+ this.retained -= trim;
284666
+ break;
284667
+ }
284668
+ }
284669
+ }
284670
+ /** The retained content as a single string (for reads/search). */
284671
+ content() {
284672
+ return this.chunks.join("");
284673
+ }
284674
+ /** Retained size in chars. */
284675
+ size() {
284676
+ return this.retained;
284677
+ }
284678
+ /** Number of chunks (for tests/diagnostics). */
284679
+ chunkCount() {
284680
+ return this.chunks.length;
284681
+ }
284682
+ clear() {
284683
+ this.chunks = [];
284684
+ this.retained = 0;
284685
+ this.offset = 0;
284686
+ }
284687
+ };
284688
+
284379
284689
  // ../../node_modules/uuid/dist/esm-node/rng.js
284380
284690
  var import_crypto = __toESM(require("crypto"));
284381
284691
  var rnds8Pool = new Uint8Array(256);
@@ -289265,6 +289575,20 @@ function stripInternalControlMarkers(s) {
289265
289575
  function stripTerminalControlSequences(s) {
289266
289576
  return s.replace(ANSI_OSC_SEQUENCE_PATTERN, "").replace(ANSI_CSI_SEQUENCE_PATTERN, "").replace(OTHER_CONTROL_CHAR_PATTERN, "");
289267
289577
  }
289578
+ function makeRingBuffer(maxSize) {
289579
+ const buf = new ChunkedRingBuffer({ maxSize });
289580
+ return {
289581
+ get content() {
289582
+ return buf.content();
289583
+ },
289584
+ get offset() {
289585
+ return buf.offset;
289586
+ },
289587
+ set offset(_v) {
289588
+ },
289589
+ append: (data) => buf.append(data)
289590
+ };
289591
+ }
289268
289592
  var cloneTerminalConfig = (config2) => JSON.parse(JSON.stringify(config2));
289269
289593
  var normalizeTerminalConfigForRuntime = (config2) => {
289270
289594
  const normalized = cloneTerminalConfig(config2);
@@ -289327,6 +289651,7 @@ var TerminalService = class {
289327
289651
  commandTrackingPromptSyncPollIntervalMs = 50;
289328
289652
  syntheticCommandQuietWindowMs = 1e3;
289329
289653
  terminalIdsBeingKilled = /* @__PURE__ */ new Set();
289654
+ autoReconnect = new AutoReconnect({ maxAttempts: 10 });
289330
289655
  terminalClosedListeners = /* @__PURE__ */ new Set();
289331
289656
  /** Optional session logger (records terminal output to disk per session). */
289332
289657
  sessionLogger = null;
@@ -289683,7 +290008,7 @@ var TerminalService = class {
289683
290008
  this.terminals.set(config2.id, tab);
289684
290009
  this.terminalConfigs.set(config2.id, config2);
289685
290010
  this.releaseReservedTitleForTerminal(config2.id, reservedTitle);
289686
- this.buffers.set(config2.id, { content: "", offset: 0 });
290011
+ this.buffers.set(config2.id, makeRingBuffer(MAX_BUFFER_SIZE));
289687
290012
  this.headlessPtys.set(config2.id, headless);
289688
290013
  if (config2.type === "local" && !this.primaryLocalTerminalId) {
289689
290014
  this.primaryLocalTerminalId = config2.id;
@@ -289766,6 +290091,8 @@ var TerminalService = class {
289766
290091
  runtime.ptyId
289767
290092
  );
289768
290093
  this.hydrateTerminalRuntimeMetadata(terminalId);
290094
+ this.autoReconnect.clear(terminalId);
290095
+ tab.reconnectState = void 0;
289769
290096
  this.publishTerminalTabsChanged();
289770
290097
  this.schedulePersistTerminalState();
289771
290098
  return tab;
@@ -289778,6 +290105,43 @@ var TerminalService = class {
289778
290105
  throw error40;
289779
290106
  }
289780
290107
  }
290108
+ /** Schedule an auto-reconnect for a dropped SSH tab with backoff. Updates
290109
+ * tab.reconnectState so the UI can show "reconnecting (attempt N)…". */
290110
+ scheduleSshAutoReconnect(terminalId) {
290111
+ const tab = this.terminals.get(terminalId);
290112
+ if (!tab || tab.type !== "ssh") return;
290113
+ const state = this.autoReconnect.schedule(
290114
+ terminalId,
290115
+ () => {
290116
+ const t = this.terminals.get(terminalId);
290117
+ if (!t || t.type !== "ssh") return;
290118
+ void this.reconnectTerminal(terminalId).catch(() => {
290119
+ });
290120
+ },
290121
+ (attempts) => {
290122
+ const t = this.terminals.get(terminalId);
290123
+ if (t) {
290124
+ t.reconnectState = {
290125
+ scheduled: false,
290126
+ attempt: attempts,
290127
+ attempts,
290128
+ nextDelayMs: 0,
290129
+ gaveUp: true
290130
+ };
290131
+ this.publishTerminalTabsChanged();
290132
+ }
290133
+ }
290134
+ );
290135
+ if (state && tab) {
290136
+ tab.reconnectState = {
290137
+ scheduled: true,
290138
+ attempt: state.nextAttempt,
290139
+ attempts: state.attemptsFired,
290140
+ nextDelayMs: state.nextDelayMs
290141
+ };
290142
+ this.publishTerminalTabsChanged();
290143
+ }
290144
+ }
289781
290145
  async restartLocalTerminalAfterExit(terminalId, code) {
289782
290146
  const tab = this.terminals.get(terminalId);
289783
290147
  const existingConfig = this.terminalConfigs.get(terminalId);
@@ -289889,13 +290253,8 @@ var TerminalService = class {
289889
290253
  const buffer = this.buffers.get(terminalId);
289890
290254
  let currentOffset = 0;
289891
290255
  if (buffer) {
289892
- buffer.content += cleanedData;
289893
- buffer.offset += cleanedData.length;
290256
+ buffer.append(cleanedData);
289894
290257
  currentOffset = buffer.offset;
289895
- if (buffer.content.length > MAX_BUFFER_SIZE) {
289896
- const trimAmount = buffer.content.length - MAX_BUFFER_SIZE;
289897
- buffer.content = buffer.content.slice(trimAmount);
289898
- }
289899
290258
  }
289900
290259
  this.sendToRenderer("terminal:data", { terminalId, data: cleanedData, offset: currentOffset });
289901
290260
  }
@@ -289926,13 +290285,8 @@ var TerminalService = class {
289926
290285
  const buffer = this.buffers.get(terminalId);
289927
290286
  let currentOffset = 0;
289928
290287
  if (buffer) {
289929
- buffer.content += data;
289930
- buffer.offset += data.length;
290288
+ buffer.append(data);
289931
290289
  currentOffset = buffer.offset;
289932
- if (buffer.content.length > MAX_BUFFER_SIZE) {
289933
- const trimAmount = buffer.content.length - MAX_BUFFER_SIZE;
289934
- buffer.content = buffer.content.slice(trimAmount);
289935
- }
289936
290290
  }
289937
290291
  this.sendToRenderer("terminal:data", { terminalId, data, offset: currentOffset });
289938
290292
  }
@@ -290144,6 +290498,9 @@ ${promptPrefix}`;
290144
290498
  rows: tab.rows
290145
290499
  });
290146
290500
  }
290501
+ if (tab.type === "ssh" && !this.terminalIdsBeingKilled.has(terminalId)) {
290502
+ this.scheduleSshAutoReconnect(terminalId);
290503
+ }
290147
290504
  }
290148
290505
  this.sendToRenderer("terminal:exit", { terminalId, code });
290149
290506
  this.publishTerminalTabsChanged();
@@ -290223,6 +290580,7 @@ ${promptPrefix}`;
290223
290580
  }
290224
290581
  kill(terminalId) {
290225
290582
  this.pendingResizeByTerminal.delete(terminalId);
290583
+ this.autoReconnect.clear(terminalId);
290226
290584
  const terminal = this.terminals.get(terminalId);
290227
290585
  if (terminal) {
290228
290586
  const backend = this.getBackend(terminal.type);
@@ -342422,6 +342780,109 @@ async function runReadFile(args, context2, readFileSupport) {
342422
342780
  }
342423
342781
  }
342424
342782
 
342783
+ // ../../packages/backend/src/memory/memoryManager.ts
342784
+ function parseMemoryEntries(content) {
342785
+ const out = [];
342786
+ const lines = String(content || "").replace(/\r\n/g, "\n").split("\n");
342787
+ for (const line of lines) {
342788
+ const t = line.trim();
342789
+ if (!t || t === "# Memory" || t.startsWith("- Add durable cross-session notes")) continue;
342790
+ if (/^#{1,6}\s/.test(t) || /^[-*]\s/.test(t) || t.length > 24) {
342791
+ out.push({ text: t, tokens: tokenize2(t) });
342792
+ }
342793
+ }
342794
+ return out;
342795
+ }
342796
+ function tokenize2(s) {
342797
+ return (s.toLowerCase().match(/[a-z0-9_.\-/]{3,}/g) ?? []).filter(
342798
+ (w) => !STOP.has(w)
342799
+ );
342800
+ }
342801
+ var STOP = /* @__PURE__ */ new Set([
342802
+ "the",
342803
+ "and",
342804
+ "for",
342805
+ "with",
342806
+ "this",
342807
+ "that",
342808
+ "from",
342809
+ "are",
342810
+ "was",
342811
+ "were",
342812
+ "has",
342813
+ "have",
342814
+ "not",
342815
+ "now",
342816
+ "via",
342817
+ "into",
342818
+ "all",
342819
+ "out",
342820
+ "use",
342821
+ "using"
342822
+ ]);
342823
+ function searchMemory(content, query, limit2 = 10) {
342824
+ const q = tokenize2(query);
342825
+ if (q.length === 0) return [];
342826
+ const qSet = new Set(q);
342827
+ const scored = [];
342828
+ for (const e of parseMemoryEntries(content)) {
342829
+ let score = 0;
342830
+ for (const tok of e.tokens) if (qSet.has(tok)) score += 1;
342831
+ if (score > 0) scored.push({ text: e.text, score });
342832
+ }
342833
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit2);
342834
+ }
342835
+ function normKey(s) {
342836
+ return s.toLowerCase().replace(/^\s*[-*+]\s+/, "").replace(/\s+/g, " ").trim();
342837
+ }
342838
+ function appendMemoryNote(content, note, opts = {}) {
342839
+ const maxChars = Math.max(1e3, opts.maxChars ?? 4e4);
342840
+ const body = String(content || "").replace(/\r\n/g, "\n").replace(/\s+$/, "");
342841
+ const noteLine = note.trim().replace(/\n+/g, " ").trim();
342842
+ const key = normKey(noteLine);
342843
+ const lines = body.split("\n");
342844
+ const kept = lines.filter((l) => normKey(l) !== key);
342845
+ const next = [...kept, "", noteLine].join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
342846
+ if (next.length <= maxChars) return next;
342847
+ const head = [];
342848
+ const tail4 = [];
342849
+ const all = next.split("\n");
342850
+ let i = 0;
342851
+ for (; i < all.length; i += 1) {
342852
+ if (/^#\s/.test(all[i]) || all[i].trim() === "") head.push(all[i]);
342853
+ else break;
342854
+ }
342855
+ for (let j = all.length - 1; j >= i; j -= 1) {
342856
+ tail4.unshift(all[j]);
342857
+ if (tail4.join("\n").length > maxChars * 0.9) break;
342858
+ }
342859
+ return [...head, ...tail4].join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
342860
+ }
342861
+ function recallForPrompt(content, opts = {}) {
342862
+ const maxChars = Math.max(2e3, opts.maxChars ?? 12e3);
342863
+ const body = String(content || "");
342864
+ if (body.length <= maxChars) return body;
342865
+ const hits = opts.query ? searchMemory(body, opts.query, 30) : [];
342866
+ const picked = [];
342867
+ let total = 0;
342868
+ const push2 = (t) => {
342869
+ if (total + t.length + 1 > maxChars) return false;
342870
+ picked.push(t);
342871
+ total += t.length + 1;
342872
+ return true;
342873
+ };
342874
+ if (hits.length > 0) {
342875
+ for (const h of hits) if (!push2(h.text)) break;
342876
+ } else {
342877
+ const entries = parseMemoryEntries(body);
342878
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
342879
+ if (!push2(entries[i].text)) break;
342880
+ }
342881
+ picked.reverse();
342882
+ }
342883
+ return picked.join("\n");
342884
+ }
342885
+
342425
342886
  // ../../packages/backend/src/services/AgentHelper/prompts.ts
342426
342887
  var SYS_INFO_MARKER = "CURRENT_SYSTEM_INFO_MSG:\n";
342427
342888
  var GYSHELL_BASE_SYSTEM_MARKER = "# Role: GyShell Assistant";
@@ -343021,14 +343482,20 @@ function buildMemoryPromptBlock(opts) {
343021
343482
  /\r\n/g,
343022
343483
  "\n"
343023
343484
  );
343485
+ const recalled = recallForPrompt(normalizedContent, {
343486
+ query: opts.userInput,
343487
+ maxChars: 12e3
343488
+ });
343489
+ const truncated = recalled.length < normalizedContent.length;
343024
343490
  return [
343025
343491
  GLOBAL_MEMORY_TAG.trim(),
343026
343492
  `Memory file absolute path: ${opts.memoryFilePath}`,
343027
343493
  "If you need to add or modify memory, use edit_file to edit this exact file path directly. Use write_file only when intentionally replacing the full memory file.",
343028
343494
  "If you need to re-read memory later, use the read_file tool to read this exact file path directly.",
343495
+ truncated ? "(Memory file is large \u2014 only the most relevant entries are shown below; read the full file with read_file if needed.)" : "",
343029
343496
  "",
343030
- "# Full MEMORY.md Content",
343031
- normalizedContent
343497
+ truncated ? "# Relevant MEMORY.md entries" : "# Full MEMORY.md Content",
343498
+ recalled
343032
343499
  ].join("\n");
343033
343500
  }
343034
343501
  function createBaseSystemPromptText(memoryPrompt) {
@@ -345650,7 +346117,7 @@ function evalCond(expr, scope) {
345650
346117
  function cleanTag(inner) {
345651
346118
  return inner.replace(/^-|\s-$/g, "").trim();
345652
346119
  }
345653
- function tokenize2(tpl) {
346120
+ function tokenize3(tpl) {
345654
346121
  const tokenRe = /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\})/g;
345655
346122
  let lastIndex = 0;
345656
346123
  const tokens = [];
@@ -345720,7 +346187,7 @@ function splitIfElse(body) {
345720
346187
  return { ifBody, elseBody, elifs };
345721
346188
  }
345722
346189
  function renderTemplate2(tpl, vars = {}) {
345723
- const tokens = tokenize2(tpl);
346190
+ const tokens = tokenize3(tpl);
345724
346191
  function renderTokens2(toks, scope) {
345725
346192
  let s = "";
345726
346193
  let idx = 0;
@@ -347146,6 +347613,9 @@ function createObservabilityBridge(deps) {
347146
347613
  dashboardState: async () => requireObs(deps).dashboard.state(),
347147
347614
  /** dashboard summary line. */
347148
347615
  dashboardSummary: async () => requireObs(deps).dashboard.summary(),
347616
+ /** monitor-status diagnostic: why stats aren't displaying per terminal. */
347617
+ monitorStatus: async () => requireObs(deps).monitorStatus.report(),
347618
+ monitorStatusSummary: async () => ({ summary: requireObs(deps).monitorStatus.summary() }),
347149
347619
  // ── secrets vault (metadata only — never values) ────────────────────────
347150
347620
  secretsList: async (params) => requireObs(deps).secrets.list(params),
347151
347621
  secretsSet: async (params) => {
@@ -347291,6 +347761,8 @@ var OBSERVABILITY_METHODS = [
347291
347761
  "observability:metricsPrometheus",
347292
347762
  "observability:dashboardState",
347293
347763
  "observability:dashboardSummary",
347764
+ "observability:monitorStatus",
347765
+ "observability:monitorStatusSummary",
347294
347766
  "observability:secretsList",
347295
347767
  "observability:secretsSet",
347296
347768
  "observability:secretsDelete",
@@ -347678,6 +348150,21 @@ async function getCloudInventory(args, context2) {
347678
348150
  emit9(context2, "get_cloud_inventory", args, out);
347679
348151
  return out;
347680
348152
  }
348153
+ var getMonitorStatusSchema = external_exports.object({
348154
+ format: external_exports.enum(["report", "summary"]).optional().describe("'report' = full per-terminal JSON; 'summary' = compact text (default).")
348155
+ });
348156
+ async function getMonitorStatus(args, context2) {
348157
+ const { o, msg } = obs(context2, "get_monitor_status", args);
348158
+ if (!o) return msg;
348159
+ let out;
348160
+ if (args.format === "report") {
348161
+ out = JSON.stringify(o.monitorStatus.report(), null, 2);
348162
+ } else {
348163
+ out = o.monitorStatus.summary();
348164
+ }
348165
+ emit9(context2, "get_monitor_status", args, out);
348166
+ return out;
348167
+ }
347681
348168
  var getLiveDashboardSchema = external_exports.object({
347682
348169
  action: external_exports.enum(["state", "subscribers"]).describe("'state' = current dashboard state; 'subscribers' = connected client count.")
347683
348170
  });
@@ -348149,6 +348636,11 @@ function buildToolsForModel(readFileSupport) {
348149
348636
  description: "Cloud resource inventory (AWS/GCP/Azure) \u2014 summary counts by provider/state, query instances (filter by provider/state/region), or pull fresh inventory.",
348150
348637
  schema: getCloudInventorySchema
348151
348638
  },
348639
+ {
348640
+ name: "get_monitor_status",
348641
+ description: "Monitor-status diagnostic \u2014 reports why stats aren't displaying per terminal (publisher wired, session exists, collection stuck in-flight, platform, last-collect age).",
348642
+ schema: getMonitorStatusSchema
348643
+ },
348152
348644
  {
348153
348645
  name: "get_live_dashboard",
348154
348646
  description: "Live multi-client dashboard \u2014 read the current unified dashboard state/summary, or the number of connected dashboard subscribers.",
@@ -348232,6 +348724,7 @@ var toolImplementations = {
348232
348724
  manageGitops,
348233
348725
  managePlaybookVersion,
348234
348726
  getCloudInventory,
348727
+ getMonitorStatus,
348235
348728
  getLiveDashboard,
348236
348729
  ingestApmSpans,
348237
348730
  getApmSummary,
@@ -368047,6 +368540,33 @@ var WebSocketGatewayAdapter = class {
368047
368540
  }
368048
368541
  return await this.options.memoryBridge.setContent(content);
368049
368542
  }
368543
+ case "memory:search": {
368544
+ if (!this.options.memoryBridge?.search) {
368545
+ throw new WebSocketRpcError(
368546
+ "METHOD_NOT_FOUND",
368547
+ "memory:search is not available on this websocket gateway."
368548
+ );
368549
+ }
368550
+ const query = params.query;
368551
+ if (typeof query !== "string") {
368552
+ throw new WebSocketRpcError("BAD_REQUEST", "query must be string.");
368553
+ }
368554
+ const limit2 = typeof params.limit === "number" ? params.limit : void 0;
368555
+ return { results: await this.options.memoryBridge.search(query, limit2) };
368556
+ }
368557
+ case "memory:append": {
368558
+ if (!this.options.memoryBridge?.append) {
368559
+ throw new WebSocketRpcError(
368560
+ "METHOD_NOT_FOUND",
368561
+ "memory:append is not available on this websocket gateway."
368562
+ );
368563
+ }
368564
+ const note = params.note;
368565
+ if (typeof note !== "string" || note.trim() === "") {
368566
+ throw new WebSocketRpcError("BAD_REQUEST", "note must be a non-empty string.");
368567
+ }
368568
+ return await this.options.memoryBridge.append(note);
368569
+ }
368050
368570
  case "agentSettings:get": {
368051
368571
  if (!this.options.agentSettingsBridge?.get) {
368052
368572
  throw new WebSocketRpcError(
@@ -386868,6 +387388,7 @@ var ResourceMonitorService = class {
386868
387388
  }
386869
387389
  const sourceTerminalId = session.sourceTerminalId || terminalIds[0];
386870
387390
  const snapshot = await this.collectSnapshot(sourceTerminalId);
387391
+ session.lastCollectAt = Date.now();
386871
387392
  if (this.publisher) {
386872
387393
  terminalIds.forEach((terminalId) => {
386873
387394
  this.publisher?.(
@@ -389054,6 +389575,20 @@ async function startGyBackend() {
389054
389575
  );
389055
389576
  gatewayService.broadcastRaw("memory:updated", snapshot);
389056
389577
  return snapshot;
389578
+ },
389579
+ search: async (query, limit2) => {
389580
+ const { content } = await memoryService.getMemorySnapshot(
389581
+ settingsService.getSettings().agentSettings?.activeProfileId || null
389582
+ );
389583
+ return searchMemory(content, query, limit2 ?? 10);
389584
+ },
389585
+ append: async (note) => {
389586
+ const profileId = settingsService.getSettings().agentSettings?.activeProfileId || null;
389587
+ const { content } = await memoryService.getMemorySnapshot(profileId);
389588
+ const next = appendMemoryNote(content, note);
389589
+ const snapshot = await memoryService.writeMemory(next, profileId);
389590
+ gatewayService.broadcastRaw("memory:updated", snapshot);
389591
+ return snapshot;
389057
389592
  }
389058
389593
  },
389059
389594
  agentSettingsBridge: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rterm-backend",
3
- "version": "3.0.3",
4
- "description": "rterm-backend — the headless AI-native backend for RTerm (dual-published as neuralOS). run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v3.0.3: fixes /dashboard \"Upgrade Required\" on the desktop app (Electron main now serves it too).",
3
+ "version": "3.0.5",
4
+ "description": "rterm-backend — the headless AI-native backend for RTerm (dual-published as neuralOS). run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v3.0.5: terminal core (SSH auto-reconnect, WinRM persistent+streaming, serial break), chat user-message nav, reconnecting indicator, memory search/cap.",
5
5
  "main": "bin/gybackend.cjs",
6
6
  "bin": {
7
7
  "gybackend": "bin/gybackend.cjs",