neuralos 3.2.18 → 3.2.19

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 +1739 -612
  2. package/package.json +2 -2
package/bin/gybackend.cjs CHANGED
@@ -15957,7 +15957,11 @@ function resolveHistoryStorageDir() {
15957
15957
  if (overrideDir) {
15958
15958
  return import_node_path4.default.resolve(overrideDir);
15959
15959
  }
15960
- return import_node_path4.default.join(process.cwd(), ".gybackend-data");
15960
+ const gybackendDataDir = (process.env.GYBACKEND_DATA_DIR || "").trim();
15961
+ if (gybackendDataDir) {
15962
+ return import_node_path4.default.resolve(gybackendDataDir);
15963
+ }
15964
+ return import_node_path4.default.join(import_node_os.default.homedir(), ".gybackend-data");
15961
15965
  }
15962
15966
  function ensureHistoryStorageDir(baseDir = resolveHistoryStorageDir()) {
15963
15967
  import_node_fs2.default.mkdirSync(baseDir, { recursive: true });
@@ -15978,11 +15982,12 @@ function resolveHistoryStoragePaths(baseDir = resolveHistoryStorageDir()) {
15978
15982
  )
15979
15983
  };
15980
15984
  }
15981
- var import_node_fs2, import_node_path4, HISTORY_SQLITE_FILE_NAME, LEGACY_CHAT_HISTORY_FILE_NAME, LEGACY_UI_HISTORY_FILE_NAME;
15985
+ var import_node_fs2, import_node_os, import_node_path4, HISTORY_SQLITE_FILE_NAME, LEGACY_CHAT_HISTORY_FILE_NAME, LEGACY_UI_HISTORY_FILE_NAME;
15982
15986
  var init_historyStoragePaths = __esm({
15983
15987
  "../../packages/backend/src/services/history/historyStoragePaths.ts"() {
15984
15988
  "use strict";
15985
15989
  import_node_fs2 = __toESM(require("node:fs"), 1);
15990
+ import_node_os = __toESM(require("node:os"), 1);
15986
15991
  import_node_path4 = __toESM(require("node:path"), 1);
15987
15992
  HISTORY_SQLITE_FILE_NAME = "gyshell-history.sqlite";
15988
15993
  LEGACY_CHAT_HISTORY_FILE_NAME = "gyshell-chat-history.json";
@@ -27263,11 +27268,11 @@ var require_pdf_worker = __commonJS({
27263
27268
  var nameTree = new NameTree(nameTreeRef, xref);
27264
27269
  var names = nameTree.getAll();
27265
27270
  for (var name in names) {
27266
- var fs22 = new FileSpec(names[name], xref);
27271
+ var fs23 = new FileSpec(names[name], xref);
27267
27272
  if (!attachments) {
27268
27273
  attachments = /* @__PURE__ */ Object.create(null);
27269
27274
  }
27270
- attachments[(0, _util.stringToPDFString)(name)] = fs22.serializable;
27275
+ attachments[(0, _util.stringToPDFString)(name)] = fs23.serializable;
27271
27276
  }
27272
27277
  }
27273
27278
  return (0, _util.shadow)(this, "attachments", attachments);
@@ -77119,7 +77124,7 @@ var require_pdf = __commonJS({
77119
77124
  throw new TypeError("Cannot call a class as a function");
77120
77125
  }
77121
77126
  }
77122
- var fs22 = require("fs");
77127
+ var fs23 = require("fs");
77123
77128
  var http2 = require("http");
77124
77129
  var https2 = require("https");
77125
77130
  var url2 = require("url");
@@ -77453,7 +77458,7 @@ var require_pdf = __commonJS({
77453
77458
  _classCallCheck(this, PDFNodeStreamFsFullReader2);
77454
77459
  var _this7 = _possibleConstructorReturn(this, (PDFNodeStreamFsFullReader2.__proto__ || Object.getPrototypeOf(PDFNodeStreamFsFullReader2)).call(this, stream));
77455
77460
  var path32 = decodeURI(_this7._url.path);
77456
- fs22.lstat(path32, function(error40, stat) {
77461
+ fs23.lstat(path32, function(error40, stat) {
77457
77462
  if (error40) {
77458
77463
  _this7._errored = true;
77459
77464
  _this7._reason = error40;
@@ -77461,7 +77466,7 @@ var require_pdf = __commonJS({
77461
77466
  return;
77462
77467
  }
77463
77468
  _this7._contentLength = stat.size;
77464
- _this7._setReadableStream(fs22.createReadStream(path32));
77469
+ _this7._setReadableStream(fs23.createReadStream(path32));
77465
77470
  _this7._headersCapability.resolve();
77466
77471
  });
77467
77472
  return _this7;
@@ -77473,7 +77478,7 @@ var require_pdf = __commonJS({
77473
77478
  function PDFNodeStreamFsRangeReader2(stream, start, end) {
77474
77479
  _classCallCheck(this, PDFNodeStreamFsRangeReader2);
77475
77480
  var _this8 = _possibleConstructorReturn(this, (PDFNodeStreamFsRangeReader2.__proto__ || Object.getPrototypeOf(PDFNodeStreamFsRangeReader2)).call(this, stream));
77476
- _this8._setReadableStream(fs22.createReadStream(decodeURI(_this8._url.path), {
77481
+ _this8._setReadableStream(fs23.createReadStream(decodeURI(_this8._url.path), {
77477
77482
  start,
77478
77483
  end: end - 1
77479
77484
  }));
@@ -89467,11 +89472,11 @@ var require_pdf_worker2 = __commonJS({
89467
89472
  var nameTree = new NameTree(nameTreeRef, xref);
89468
89473
  var names = nameTree.getAll();
89469
89474
  for (var name in names) {
89470
- var fs22 = new FileSpec(names[name], xref);
89475
+ var fs23 = new FileSpec(names[name], xref);
89471
89476
  if (!attachments) {
89472
89477
  attachments = /* @__PURE__ */ Object.create(null);
89473
89478
  }
89474
- attachments[(0, _util.stringToPDFString)(name)] = fs22.serializable;
89479
+ attachments[(0, _util.stringToPDFString)(name)] = fs23.serializable;
89475
89480
  }
89476
89481
  }
89477
89482
  return (0, _util.shadow)(this, "attachments", attachments);
@@ -138955,7 +138960,7 @@ var require_pdf2 = __commonJS({
138955
138960
  throw new TypeError("Cannot call a class as a function");
138956
138961
  }
138957
138962
  }
138958
- var fs22 = require("fs");
138963
+ var fs23 = require("fs");
138959
138964
  var http2 = require("http");
138960
138965
  var https2 = require("https");
138961
138966
  var url2 = require("url");
@@ -139289,7 +139294,7 @@ var require_pdf2 = __commonJS({
139289
139294
  _classCallCheck(this, PDFNodeStreamFsFullReader2);
139290
139295
  var _this7 = _possibleConstructorReturn(this, (PDFNodeStreamFsFullReader2.__proto__ || Object.getPrototypeOf(PDFNodeStreamFsFullReader2)).call(this, stream));
139291
139296
  var path32 = decodeURI(_this7._url.path);
139292
- fs22.lstat(path32, function(error40, stat) {
139297
+ fs23.lstat(path32, function(error40, stat) {
139293
139298
  if (error40) {
139294
139299
  _this7._errored = true;
139295
139300
  _this7._reason = error40;
@@ -139297,7 +139302,7 @@ var require_pdf2 = __commonJS({
139297
139302
  return;
139298
139303
  }
139299
139304
  _this7._contentLength = stat.size;
139300
- _this7._setReadableStream(fs22.createReadStream(path32));
139305
+ _this7._setReadableStream(fs23.createReadStream(path32));
139301
139306
  _this7._headersCapability.resolve();
139302
139307
  });
139303
139308
  return _this7;
@@ -139309,7 +139314,7 @@ var require_pdf2 = __commonJS({
139309
139314
  function PDFNodeStreamFsRangeReader2(stream, start, end) {
139310
139315
  _classCallCheck(this, PDFNodeStreamFsRangeReader2);
139311
139316
  var _this8 = _possibleConstructorReturn(this, (PDFNodeStreamFsRangeReader2.__proto__ || Object.getPrototypeOf(PDFNodeStreamFsRangeReader2)).call(this, stream));
139312
- _this8._setReadableStream(fs22.createReadStream(decodeURI(_this8._url.path), {
139317
+ _this8._setReadableStream(fs23.createReadStream(decodeURI(_this8._url.path), {
139313
139318
  start,
139314
139319
  end: end - 1
139315
139320
  }));
@@ -161693,11 +161698,11 @@ var require_pdf_worker3 = __commonJS({
161693
161698
  var nameTree = new NameTree(nameTreeRef, xref);
161694
161699
  var names = nameTree.getAll();
161695
161700
  for (var name in names) {
161696
- var fs22 = new FileSpec(names[name], xref);
161701
+ var fs23 = new FileSpec(names[name], xref);
161697
161702
  if (!attachments) {
161698
161703
  attachments = /* @__PURE__ */ Object.create(null);
161699
161704
  }
161700
- attachments[(0, _util.stringToPDFString)(name)] = fs22.serializable;
161705
+ attachments[(0, _util.stringToPDFString)(name)] = fs23.serializable;
161701
161706
  }
161702
161707
  }
161703
161708
  return (0, _util.shadow)(this, "attachments", attachments);
@@ -203578,11 +203583,11 @@ var require_pdf_worker4 = __commonJS({
203578
203583
  var nameTree = new NameTree(nameTreeRef, xref);
203579
203584
  var names = nameTree.getAll();
203580
203585
  for (var name in names) {
203581
- var fs22 = new FileSpec(names[name], xref);
203586
+ var fs23 = new FileSpec(names[name], xref);
203582
203587
  if (!attachments) {
203583
203588
  attachments = /* @__PURE__ */ Object.create(null);
203584
203589
  }
203585
- attachments[(0, _util.stringToPDFString)(name)] = fs22.serializable;
203590
+ attachments[(0, _util.stringToPDFString)(name)] = fs23.serializable;
203586
203591
  }
203587
203592
  }
203588
203593
  return (0, _util.shadow)(this, "attachments", attachments);
@@ -253965,7 +253970,7 @@ var require_pdf4 = __commonJS({
253965
253970
  throw new TypeError("Cannot call a class as a function");
253966
253971
  }
253967
253972
  }
253968
- var fs22 = require("fs");
253973
+ var fs23 = require("fs");
253969
253974
  var http2 = require("http");
253970
253975
  var https2 = require("https");
253971
253976
  var url2 = require("url");
@@ -254323,7 +254328,7 @@ var require_pdf4 = __commonJS({
254323
254328
  if (fileUriRegex.test(_this7._url.href)) {
254324
254329
  path32 = path32.replace(/^\//, "");
254325
254330
  }
254326
- fs22.lstat(path32, function(error40, stat) {
254331
+ fs23.lstat(path32, function(error40, stat) {
254327
254332
  if (error40) {
254328
254333
  _this7._errored = true;
254329
254334
  _this7._reason = error40;
@@ -254331,7 +254336,7 @@ var require_pdf4 = __commonJS({
254331
254336
  return;
254332
254337
  }
254333
254338
  _this7._contentLength = stat.size;
254334
- _this7._setReadableStream(fs22.createReadStream(path32));
254339
+ _this7._setReadableStream(fs23.createReadStream(path32));
254335
254340
  _this7._headersCapability.resolve();
254336
254341
  });
254337
254342
  return _this7;
@@ -254347,7 +254352,7 @@ var require_pdf4 = __commonJS({
254347
254352
  if (fileUriRegex.test(_this8._url.href)) {
254348
254353
  path32 = path32.replace(/^\//, "");
254349
254354
  }
254350
- _this8._setReadableStream(fs22.createReadStream(path32, {
254355
+ _this8._setReadableStream(fs23.createReadStream(path32, {
254351
254356
  start,
254352
254357
  end: end - 1
254353
254358
  }));
@@ -255452,6 +255457,105 @@ var require_pdf_parse2 = __commonJS({
255452
255457
  }
255453
255458
  });
255454
255459
 
255460
+ // ../../packages/backend/src/services/AgentHelper/utils/subAgent.ts
255461
+ var subAgent_exports = {};
255462
+ __export(subAgent_exports, {
255463
+ renderSubAgentSummary: () => renderSubAgentSummary,
255464
+ runSubAgents: () => runSubAgents,
255465
+ validateSubAgentSpec: () => validateSubAgentSpec
255466
+ });
255467
+ function validateSubAgentSpec(spec) {
255468
+ if (!spec || typeof spec !== "object") return { ok: false, error: "spec must be an object" };
255469
+ const s = spec;
255470
+ if (!Array.isArray(s.tasks) || s.tasks.length === 0) {
255471
+ return { ok: false, error: "tasks must be a non-empty array" };
255472
+ }
255473
+ if (s.tasks.length > 20) {
255474
+ return { ok: false, error: `too many tasks (${s.tasks.length}); max 20 per fan-out` };
255475
+ }
255476
+ for (let i = 0; i < s.tasks.length; i++) {
255477
+ const t = s.tasks[i];
255478
+ if (!t || typeof t !== "object") return { ok: false, error: `task ${i + 1} must be an object` };
255479
+ if (typeof t.prompt !== "string" || !t.prompt.trim()) {
255480
+ return { ok: false, error: `task ${i + 1} needs a non-empty prompt` };
255481
+ }
255482
+ if (t.label !== void 0 && typeof t.label !== "string") {
255483
+ return { ok: false, error: `task ${i + 1} label must be a string` };
255484
+ }
255485
+ }
255486
+ if (s.maxConcurrent !== void 0 && (!Number.isFinite(s.maxConcurrent) || s.maxConcurrent < 1)) {
255487
+ return { ok: false, error: "maxConcurrent must be >= 1" };
255488
+ }
255489
+ if (s.timeoutMs !== void 0 && (!Number.isFinite(s.timeoutMs) || s.timeoutMs < 1e3)) {
255490
+ return { ok: false, error: "timeoutMs must be >= 1000" };
255491
+ }
255492
+ return { ok: true };
255493
+ }
255494
+ async function runWithTimeout(task2, run, timeoutMs) {
255495
+ const started = Date.now();
255496
+ return await new Promise((resolve2) => {
255497
+ let settled = false;
255498
+ const finish = (r) => {
255499
+ if (!settled) {
255500
+ settled = true;
255501
+ clearTimeout(timer);
255502
+ resolve2(r);
255503
+ }
255504
+ };
255505
+ const timer = setTimeout(() => {
255506
+ finish({ label: task2.label, ok: false, error: "timed out", durationMs: Date.now() - started, timedOut: true });
255507
+ }, timeoutMs);
255508
+ run(task2).then((output) => finish({ label: task2.label, ok: true, output, durationMs: Date.now() - started, timedOut: false })).catch((e) => finish({
255509
+ label: task2.label,
255510
+ ok: false,
255511
+ error: e instanceof Error ? e.message : String(e),
255512
+ durationMs: Date.now() - started,
255513
+ timedOut: false
255514
+ }));
255515
+ });
255516
+ }
255517
+ async function runSubAgents(spec, run) {
255518
+ const maxConcurrent = Math.max(1, spec.maxConcurrent ?? 3);
255519
+ const timeoutMs = spec.timeoutMs ?? 3e5;
255520
+ const results = [];
255521
+ for (let i = 0; i < spec.tasks.length; i += maxConcurrent) {
255522
+ const batch = spec.tasks.slice(i, i + maxConcurrent);
255523
+ const batchResults = await Promise.all(
255524
+ batch.map((t) => runWithTimeout(t, run, timeoutMs))
255525
+ );
255526
+ results.push(...batchResults);
255527
+ }
255528
+ const succeeded = results.filter((r) => r.ok).length;
255529
+ return {
255530
+ ok: succeeded === results.length,
255531
+ total: results.length,
255532
+ succeeded,
255533
+ failed: results.length - succeeded,
255534
+ results
255535
+ };
255536
+ }
255537
+ function renderSubAgentSummary(summary) {
255538
+ const lines = [
255539
+ `Sub-agent fan-out: ${summary.succeeded}/${summary.total} succeeded.`,
255540
+ ""
255541
+ ];
255542
+ for (const r of summary.results) {
255543
+ const status = r.ok ? "\u2713" : r.timedOut ? "\u23F1" : "\u2717";
255544
+ const detail = r.ok ? (r.output ?? "").slice(0, 400) : `${r.error ?? "failed"}${r.timedOut ? ` (after ${Math.round(r.durationMs / 1e3)}s)` : ""}`;
255545
+ lines.push(`${status} ${r.label}: ${detail}`);
255546
+ lines.push("");
255547
+ }
255548
+ if (summary.failed > 0) {
255549
+ lines.push(`${summary.failed} task(s) failed \u2014 consider retrying those individually.`);
255550
+ }
255551
+ return lines.join("\n");
255552
+ }
255553
+ var init_subAgent = __esm({
255554
+ "../../packages/backend/src/services/AgentHelper/utils/subAgent.ts"() {
255555
+ "use strict";
255556
+ }
255557
+ });
255558
+
255455
255559
  // ../../packages/backend/src/services/automation/templateEngine.ts
255456
255560
  var templateEngine_exports = {};
255457
255561
  __export(templateEngine_exports, {
@@ -257990,6 +258094,84 @@ var init_etwService = __esm({
257990
258094
  }
257991
258095
  });
257992
258096
 
258097
+ // ../../packages/backend/src/services/AgentHelper/utils/modelFailover.ts
258098
+ var modelFailover_exports = {};
258099
+ __export(modelFailover_exports, {
258100
+ buildFailoverChain: () => buildFailoverChain,
258101
+ isFailoverEligible: () => isFailoverEligible,
258102
+ withModelFailover: () => withModelFailover
258103
+ });
258104
+ function isFailoverEligible(error40) {
258105
+ const message = error40 instanceof Error ? error40.message : String(error40 ?? "");
258106
+ for (const re of NON_FAILOVER_PATTERNS) {
258107
+ if (re.test(message)) return { eligible: false, reason: "non-failover-error" };
258108
+ }
258109
+ for (const { re: pattern, reason } of FAILOVER_ERROR_PATTERNS) {
258110
+ if (pattern.test(message)) return { eligible: true, reason };
258111
+ }
258112
+ return { eligible: true, reason: "unknown-error" };
258113
+ }
258114
+ function buildFailoverChain(primary, fallbacks) {
258115
+ const chain = [primary];
258116
+ if (!Array.isArray(fallbacks)) return chain;
258117
+ const seen = /* @__PURE__ */ new Set([primary.model]);
258118
+ for (const f of fallbacks) {
258119
+ if (!f || !f.model) continue;
258120
+ if (seen.has(f.model)) continue;
258121
+ seen.add(f.model);
258122
+ chain.push(f);
258123
+ }
258124
+ return chain;
258125
+ }
258126
+ async function withModelFailover(chain, operation, opts = {}) {
258127
+ const attempts = [];
258128
+ let lastError;
258129
+ for (const candidate of chain) {
258130
+ const started = Date.now();
258131
+ try {
258132
+ const value = await operation(candidate);
258133
+ const attempt = { model: candidate.model, ok: true, durationMs: Date.now() - started };
258134
+ attempts.push(attempt);
258135
+ opts.onAttempt?.(attempt);
258136
+ return { value, attempts, usedModel: candidate.model };
258137
+ } catch (error40) {
258138
+ const decision = isFailoverEligible(error40);
258139
+ const attempt = {
258140
+ model: candidate.model,
258141
+ ok: false,
258142
+ reason: decision.reason,
258143
+ durationMs: Date.now() - started
258144
+ };
258145
+ attempts.push(attempt);
258146
+ opts.onAttempt?.(attempt);
258147
+ if (!decision.eligible) {
258148
+ return { error: error40, attempts };
258149
+ }
258150
+ lastError = error40;
258151
+ }
258152
+ }
258153
+ return { error: lastError, attempts };
258154
+ }
258155
+ var FAILOVER_ERROR_PATTERNS, NON_FAILOVER_PATTERNS;
258156
+ var init_modelFailover = __esm({
258157
+ "../../packages/backend/src/services/AgentHelper/utils/modelFailover.ts"() {
258158
+ "use strict";
258159
+ FAILOVER_ERROR_PATTERNS = [
258160
+ { re: /\b429\b|rate.?limit|too many requests|quota/i, reason: "rate-limited" },
258161
+ { re: /\b5\d\d\b|internal server error|bad gateway|service unavailable|upstream/i, reason: "provider-5xx" },
258162
+ { re: /network|fetch failed|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up/i, reason: "network" },
258163
+ { re: /\b401\b|unauthorized|invalid api key|authentication/i, reason: "auth" },
258164
+ { re: /overloaded|capacity|model .* not found|no such model/i, reason: "model-unavailable" }
258165
+ ];
258166
+ NON_FAILOVER_PATTERNS = [
258167
+ /context.?length|token limit|maximum context/i,
258168
+ /invalid request|malformed|bad request|\b400\b/i,
258169
+ /aborted|AbortError/i
258170
+ // user cancelled — never retry
258171
+ ];
258172
+ }
258173
+ });
258174
+
257993
258175
  // ../../packages/backend/src/services/history/HistorySqliteStore.ts
257994
258176
  var HistorySqliteStore_exports = {};
257995
258177
  __export(HistorySqliteStore_exports, {
@@ -261975,6 +262157,136 @@ var require_websocket_server = __commonJS({
261975
262157
  }
261976
262158
  });
261977
262159
 
262160
+ // ../../packages/backend/src/services/history/historySearch.ts
262161
+ var historySearch_exports = {};
262162
+ __export(historySearch_exports, {
262163
+ buildSnippet: () => buildSnippet,
262164
+ extractMessageText: () => extractMessageText2,
262165
+ findMatches: () => findMatches,
262166
+ normalizeMessages: () => normalizeMessages,
262167
+ searchChatHistory: () => searchChatHistory
262168
+ });
262169
+ function extractMessageText2(data) {
262170
+ if (!data || typeof data !== "object") return "";
262171
+ let obj = data;
262172
+ for (let i = 0; i < 3 && obj.data && typeof obj.data === "object" && !Array.isArray(obj.data); i++) {
262173
+ const inner = obj.data;
262174
+ if (inner.content !== void 0 || inner.data !== void 0) {
262175
+ obj = inner;
262176
+ } else {
262177
+ break;
262178
+ }
262179
+ }
262180
+ const content = obj.content;
262181
+ if (typeof content === "string") return content;
262182
+ if (Array.isArray(content)) {
262183
+ return content.map((part) => {
262184
+ if (typeof part === "string") return part;
262185
+ if (part && typeof part === "object") {
262186
+ const p = part;
262187
+ if (typeof p.text === "string") return p.text;
262188
+ }
262189
+ return "";
262190
+ }).join(" ");
262191
+ }
262192
+ return "";
262193
+ }
262194
+ function buildSnippet(text, offset, length) {
262195
+ const start = Math.max(0, offset - SNIPPET_CONTEXT);
262196
+ const end = Math.min(text.length, offset + length + SNIPPET_CONTEXT);
262197
+ const prefix = start > 0 ? "\u2026" : "";
262198
+ const suffix = end < text.length ? "\u2026" : "";
262199
+ return prefix + text.slice(start, end).replace(/\s+/g, " ").trim() + suffix;
262200
+ }
262201
+ function findMatches(text, query, wholeWord) {
262202
+ if (!text || !query) return [];
262203
+ const haystack = text.toLowerCase();
262204
+ const needle = query.toLowerCase();
262205
+ const out = [];
262206
+ if (wholeWord) {
262207
+ const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
262208
+ let m2;
262209
+ while ((m2 = re.exec(haystack)) !== null) {
262210
+ out.push({ offset: m2.index, length: m2[0].length });
262211
+ if (m2.index === re.lastIndex) re.lastIndex++;
262212
+ }
262213
+ } else {
262214
+ let idx = haystack.indexOf(needle);
262215
+ while (idx !== -1) {
262216
+ out.push({ offset: idx, length: needle.length });
262217
+ idx = haystack.indexOf(needle, idx + needle.length);
262218
+ }
262219
+ }
262220
+ return out;
262221
+ }
262222
+ function normalizeMessages(messages) {
262223
+ if (!messages) return [];
262224
+ if (Array.isArray(messages)) return messages;
262225
+ if (messages instanceof Map) return [...messages.values()];
262226
+ if (typeof messages === "object") return Object.values(messages);
262227
+ return [];
262228
+ }
262229
+ function searchChatHistory(sessions, query, options = {}) {
262230
+ const sessionLimit = options.sessionLimit ?? DEFAULT_SESSION_LIMIT;
262231
+ const snippetLimit = options.snippetLimit ?? DEFAULT_SNIPPET_LIMIT;
262232
+ const wholeWord = options.wholeWord ?? false;
262233
+ const includeTitles = options.includeTitles ?? true;
262234
+ const trimmed = (query ?? "").trim();
262235
+ if (!trimmed) {
262236
+ return { query: trimmed, totalSessions: 0, totalMatches: 0, sessions: [], truncated: false };
262237
+ }
262238
+ const results = [];
262239
+ let totalMatches = 0;
262240
+ for (const session of sessions) {
262241
+ let matchCount = 0;
262242
+ const matches = [];
262243
+ const titleHit = includeTitles && session.title ? findMatches(session.title, trimmed, wholeWord) : [];
262244
+ for (const message of normalizeMessages(session.messages)) {
262245
+ const text = extractMessageText2(message?.data);
262246
+ if (!text) continue;
262247
+ const hits = findMatches(text, trimmed, wholeWord);
262248
+ for (const hit of hits) {
262249
+ matchCount++;
262250
+ totalMatches++;
262251
+ if (matches.length < snippetLimit) {
262252
+ matches.push({
262253
+ messageId: String(message?.id ?? ""),
262254
+ type: String(message?.type ?? ""),
262255
+ snippet: buildSnippet(text, hit.offset, hit.length),
262256
+ offset: hit.offset
262257
+ });
262258
+ }
262259
+ }
262260
+ }
262261
+ if (matchCount === 0 && titleHit.length === 0) continue;
262262
+ results.push({
262263
+ sessionId: session.id,
262264
+ sessionTitle: session.title,
262265
+ updatedAt: session.updatedAt,
262266
+ matchCount: matchCount + titleHit.length,
262267
+ matches
262268
+ });
262269
+ }
262270
+ results.sort((a, b) => b.matchCount - a.matchCount || b.updatedAt - a.updatedAt);
262271
+ const truncated = results.length > sessionLimit;
262272
+ return {
262273
+ query: trimmed,
262274
+ totalSessions: results.length,
262275
+ totalMatches,
262276
+ sessions: results.slice(0, sessionLimit),
262277
+ truncated
262278
+ };
262279
+ }
262280
+ var SNIPPET_CONTEXT, DEFAULT_SESSION_LIMIT, DEFAULT_SNIPPET_LIMIT;
262281
+ var init_historySearch = __esm({
262282
+ "../../packages/backend/src/services/history/historySearch.ts"() {
262283
+ "use strict";
262284
+ SNIPPET_CONTEXT = 60;
262285
+ DEFAULT_SESSION_LIMIT = 20;
262286
+ DEFAULT_SNIPPET_LIMIT = 3;
262287
+ }
262288
+ });
262289
+
261978
262290
  // ../../node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js
261979
262291
  var require_code = __commonJS({
261980
262292
  "../../node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js"(exports2) {
@@ -274408,12 +274720,12 @@ var require_dist2 = __commonJS({
274408
274720
  throw new Error(`Unknown format "${name}"`);
274409
274721
  return f;
274410
274722
  };
274411
- function addFormats(ajv, list, fs22, exportName) {
274723
+ function addFormats(ajv, list, fs23, exportName) {
274412
274724
  var _a3;
274413
274725
  var _b;
274414
274726
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
274415
274727
  for (const f of list)
274416
- ajv.addFormat(f, fs22[f]);
274728
+ ajv.addFormat(f, fs23[f]);
274417
274729
  }
274418
274730
  module2.exports = exports2 = formatsPlugin;
274419
274731
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -274426,7 +274738,7 @@ var require_windows = __commonJS({
274426
274738
  "../../node_modules/isexe/windows.js"(exports2, module2) {
274427
274739
  module2.exports = isexe;
274428
274740
  isexe.sync = sync;
274429
- var fs22 = require("fs");
274741
+ var fs23 = require("fs");
274430
274742
  function checkPathExt(path32, options) {
274431
274743
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
274432
274744
  if (!pathext) {
@@ -274451,12 +274763,12 @@ var require_windows = __commonJS({
274451
274763
  return checkPathExt(path32, options);
274452
274764
  }
274453
274765
  function isexe(path32, options, cb) {
274454
- fs22.stat(path32, function(er, stat) {
274766
+ fs23.stat(path32, function(er, stat) {
274455
274767
  cb(er, er ? false : checkStat(stat, path32, options));
274456
274768
  });
274457
274769
  }
274458
274770
  function sync(path32, options) {
274459
- return checkStat(fs22.statSync(path32), path32, options);
274771
+ return checkStat(fs23.statSync(path32), path32, options);
274460
274772
  }
274461
274773
  }
274462
274774
  });
@@ -274466,14 +274778,14 @@ var require_mode = __commonJS({
274466
274778
  "../../node_modules/isexe/mode.js"(exports2, module2) {
274467
274779
  module2.exports = isexe;
274468
274780
  isexe.sync = sync;
274469
- var fs22 = require("fs");
274781
+ var fs23 = require("fs");
274470
274782
  function isexe(path32, options, cb) {
274471
- fs22.stat(path32, function(er, stat) {
274783
+ fs23.stat(path32, function(er, stat) {
274472
274784
  cb(er, er ? false : checkStat(stat, options));
274473
274785
  });
274474
274786
  }
274475
274787
  function sync(path32, options) {
274476
- return checkStat(fs22.statSync(path32), options);
274788
+ return checkStat(fs23.statSync(path32), options);
274477
274789
  }
274478
274790
  function checkStat(stat, options) {
274479
274791
  return stat.isFile() && checkMode(stat, options);
@@ -274497,7 +274809,7 @@ var require_mode = __commonJS({
274497
274809
  // ../../node_modules/isexe/index.js
274498
274810
  var require_isexe = __commonJS({
274499
274811
  "../../node_modules/isexe/index.js"(exports2, module2) {
274500
- var fs22 = require("fs");
274812
+ var fs23 = require("fs");
274501
274813
  var core2;
274502
274814
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
274503
274815
  core2 = require_windows();
@@ -274761,16 +275073,16 @@ var require_shebang_command = __commonJS({
274761
275073
  var require_readShebang = __commonJS({
274762
275074
  "../../node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
274763
275075
  "use strict";
274764
- var fs22 = require("fs");
275076
+ var fs23 = require("fs");
274765
275077
  var shebangCommand = require_shebang_command();
274766
275078
  function readShebang(command) {
274767
275079
  const size = 150;
274768
275080
  const buffer = Buffer.alloc(size);
274769
275081
  let fd;
274770
275082
  try {
274771
- fd = fs22.openSync(command, "r");
274772
- fs22.readSync(fd, buffer, 0, size, 0);
274773
- fs22.closeSync(fd);
275083
+ fd = fs23.openSync(command, "r");
275084
+ fs23.readSync(fd, buffer, 0, size, 0);
275085
+ fs23.closeSync(fd);
274774
275086
  } catch (e) {
274775
275087
  }
274776
275088
  return shebangCommand(buffer.toString());
@@ -275398,7 +275710,7 @@ var require_has_flag = __commonJS({
275398
275710
  var require_supports_color = __commonJS({
275399
275711
  "../../node_modules/supports-color/index.js"(exports2, module2) {
275400
275712
  "use strict";
275401
- var os7 = require("os");
275713
+ var os8 = require("os");
275402
275714
  var tty = require("tty");
275403
275715
  var hasFlag = require_has_flag();
275404
275716
  var { env } = process;
@@ -275446,7 +275758,7 @@ var require_supports_color = __commonJS({
275446
275758
  return min;
275447
275759
  }
275448
275760
  if (process.platform === "win32") {
275449
- const osRelease = os7.release().split(".");
275761
+ const osRelease = os8.release().split(".");
275450
275762
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
275451
275763
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
275452
275764
  }
@@ -286297,11 +286609,11 @@ var require_jsmconsumer_api = __commonJS({
286297
286609
  consumerName = cfg.name ?? cfg.durable_name ?? "";
286298
286610
  }
286299
286611
  if (consumerName !== "") {
286300
- let fs22 = cfg.filter_subject ?? void 0;
286301
- if (fs22 === ">") {
286302
- fs22 = void 0;
286612
+ let fs23 = cfg.filter_subject ?? void 0;
286613
+ if (fs23 === ">") {
286614
+ fs23 = void 0;
286303
286615
  }
286304
- subj = fs22 !== void 0 ? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs22}` : `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
286616
+ subj = fs23 !== void 0 ? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs23}` : `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
286305
286617
  } else {
286306
286618
  subj = cfg.durable_name ? `${this.prefix}.CONSUMER.DURABLE.CREATE.${stream}.${cfg.durable_name}` : `${this.prefix}.CONSUMER.CREATE.${stream}`;
286307
286619
  }
@@ -290852,6 +291164,7 @@ var init_natsEventBus = __esm({
290852
291164
 
290853
291165
  // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
290854
291166
  var import_node_path28 = __toESM(require("node:path"), 1);
291167
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
290855
291168
  var import_node_process2 = __toESM(require("node:process"), 1);
290856
291169
 
290857
291170
  // ../../packages/backend/src/services/TerminalService.ts
@@ -293693,19 +294006,19 @@ var SSHBackend = class _SSHBackend {
293693
294006
  ),
293694
294007
  this.execCollect(client2, "hostname", 8e3)
293695
294008
  ]);
293696
- let os7 = "unix";
294009
+ let os8 = "unix";
293697
294010
  const releaseMatch = osRelease.stdout.match(/^ID=(.*)$/m);
293698
294011
  if (releaseMatch) {
293699
- os7 = releaseMatch[1].replace(/"/g, "");
294012
+ os8 = releaseMatch[1].replace(/"/g, "");
293700
294013
  } else {
293701
294014
  const unameS2 = uname.stdout.split(" ")[0].toLowerCase();
293702
- os7 = unameS2 || "unix";
294015
+ os8 = unameS2 || "unix";
293703
294016
  }
293704
294017
  const unameS = uname.stdout.split(" ")[0].toLowerCase();
293705
294018
  const platform2 = unameS.includes("darwin") ? "darwin" : unameS.includes("linux") ? "linux" : "unix";
293706
294019
  const parts = uname.stdout.split(" ");
293707
294020
  const next = {
293708
- os: os7,
294021
+ os: os8,
293709
294022
  platform: platform2,
293710
294023
  release: parts[2] || "",
293711
294024
  arch: parts[parts.length - 2] || "",
@@ -300516,6 +300829,9 @@ var TerminalService = class _TerminalService {
300516
300829
  terminalIdsBeingKilled = /* @__PURE__ */ new Set();
300517
300830
  autoReconnect = new AutoReconnect({ maxAttempts: 10 });
300518
300831
  terminalClosedListeners = /* @__PURE__ */ new Set();
300832
+ /** v3.2.18: idle-timeout tracking hooks. */
300833
+ terminalCreatedListeners = /* @__PURE__ */ new Set();
300834
+ terminalDataListeners = /* @__PURE__ */ new Set();
300519
300835
  /** Optional session logger (records terminal output to disk per session). */
300520
300836
  sessionLogger = null;
300521
300837
  sessionLogStarted = /* @__PURE__ */ new Set();
@@ -300565,6 +300881,38 @@ var TerminalService = class _TerminalService {
300565
300881
  this.terminalClosedListeners.delete(listener);
300566
300882
  };
300567
300883
  }
300884
+ /** v3.2.18: fires when a terminal tab is created (idle-timeout tracking). */
300885
+ onTerminalCreated(listener) {
300886
+ this.terminalCreatedListeners.add(listener);
300887
+ return () => {
300888
+ this.terminalCreatedListeners.delete(listener);
300889
+ };
300890
+ }
300891
+ /** v3.2.18: fires on any terminal data event (idle-timeout activity). */
300892
+ onTerminalData(listener) {
300893
+ this.terminalDataListeners.add(listener);
300894
+ return () => {
300895
+ this.terminalDataListeners.delete(listener);
300896
+ };
300897
+ }
300898
+ /** v3.2.18: notify created-listeners (called from createTerminal paths). */
300899
+ notifyTerminalCreated(terminalId) {
300900
+ for (const listener of this.terminalCreatedListeners) {
300901
+ try {
300902
+ listener(terminalId);
300903
+ } catch {
300904
+ }
300905
+ }
300906
+ }
300907
+ /** v3.2.18: notify data-listeners (called from the data pump). */
300908
+ notifyTerminalData(terminalId) {
300909
+ for (const listener of this.terminalDataListeners) {
300910
+ try {
300911
+ listener(terminalId);
300912
+ } catch {
300913
+ }
300914
+ }
300915
+ }
300568
300916
  listRenderableTerminals() {
300569
300917
  return Array.from(this.terminals.values()).map((terminal) => ({
300570
300918
  id: terminal.id,
@@ -300883,6 +301231,7 @@ var TerminalService = class _TerminalService {
300883
301231
  }
300884
301232
  this.publishTerminalTabsChanged();
300885
301233
  this.schedulePersistTerminalState();
301234
+ this.notifyTerminalCreated(tab.id);
300886
301235
  return tab;
300887
301236
  }
300888
301237
  async reconnectTerminal(terminalId) {
@@ -301076,6 +301425,7 @@ var TerminalService = class _TerminalService {
301076
301425
  }
301077
301426
  this.sessionLogger.write(terminalId, captureData);
301078
301427
  }
301428
+ this.notifyTerminalData(terminalId);
301079
301429
  });
301080
301430
  }
301081
301431
  }
@@ -301397,6 +301747,7 @@ ${promptPrefix}`;
301397
301747
  const backend = this.getBackend(terminal.type);
301398
301748
  backend.write(terminal.ptyId, data);
301399
301749
  this.pendingWriteRetries.delete(terminalId);
301750
+ this.notifyTerminalData(terminalId);
301400
301751
  } else if (terminal) {
301401
301752
  if (terminal.runtimeState === "exited") {
301402
301753
  this.pendingWrites.delete(terminalId);
@@ -350077,6 +350428,11 @@ var CLOSE_TERMINAL_TAB_DESCRIPTION = [
350077
350428
  "Use this to clean up tabs the task no longer needs \u2014 e.g. after a transfer finished, or after a one-off probe on a server. Close tabs you opened yourself when the work is done; do not close tabs the user was actively using unless asked.",
350078
350429
  "Guarded: it refuses to close the last remaining terminal tab (at least one tab must stay open) and reports how many tabs remain after the close. If the tab is running a command, the command is terminated by the close."
350079
350430
  ].join("\n");
350431
+ var SPAWN_SUBAGENTS_DESCRIPTION = [
350432
+ "Fan work out to concurrent child agent sessions. Each task gets its own session running its prompt to completion; results are collected into one summary.",
350433
+ "Use this when you have MULTIPLE INDEPENDENT tasks that would otherwise run one at a time \u2014 e.g. 'check disk usage on these 6 servers', 'analyze these 3 files', or 'research these 4 questions'. Do NOT use it for sequential work where each step depends on the previous one's output.",
350434
+ "Each task needs a self-contained prompt (the child has no context from this conversation) and a short label. Guards: max 20 tasks per fan-out, per-child timeout 5 minutes (configurable), concurrency cap 3 (configurable). One child failing or timing out does not affect the others \u2014 you get a per-task result with \u2713/\u2717 status."
350435
+ ].join("\n");
350080
350436
  var COPY_BETWEEN_TABS_DESCRIPTION = [
350081
350437
  "Start an asynchronous file copy between two different terminal tabs on different machines. Use this only for cross-terminal-tab file transfer; do not use it for copying within one tab or between two tabs connected to the same machine.",
350082
350438
  "If either source or target terminal is disconnected or not ready, this tool returns an explicit terminal_status for that side and does not start a transfer.",
@@ -350214,6 +350570,11 @@ var BUILTIN_TOOL_INFO = [
350214
350570
  description: CLOSE_TERMINAL_TAB_DESCRIPTION,
350215
350571
  shortDescription: "Close a terminal tab"
350216
350572
  },
350573
+ {
350574
+ name: "spawn_subagents",
350575
+ description: SPAWN_SUBAGENTS_DESCRIPTION,
350576
+ shortDescription: "Fan out to concurrent child agents"
350577
+ },
350217
350578
  {
350218
350579
  name: "create_or_edit",
350219
350580
  description: CREATE_OR_EDIT_TOOL_DESCRIPTION,
@@ -351869,6 +352230,69 @@ async function closeTerminalTab(args, context2) {
351869
352230
  }
351870
352231
  }
351871
352232
 
352233
+ // ../../packages/backend/src/services/AgentHelper/tools/subAgent_tools.ts
352234
+ init_zod();
352235
+ var spawnSubAgentsSchema = external_exports.object({
352236
+ tasks: external_exports.array(
352237
+ external_exports.object({
352238
+ label: external_exports.string().min(1).describe('Short label for reporting, e.g. "server-1"'),
352239
+ prompt: external_exports.string().min(1).describe("The self-contained prompt for the child agent.")
352240
+ })
352241
+ ).min(1).max(20).describe("The tasks to fan out. Each child gets its own session. Max 20."),
352242
+ maxConcurrent: external_exports.number().int().min(1).max(10).optional().describe("Max children running concurrently (default 3)."),
352243
+ timeoutMs: external_exports.number().int().min(1e3).optional().describe("Per-child timeout in ms (default 300000 = 5 minutes).")
352244
+ });
352245
+ async function spawnSubAgents(args, context2) {
352246
+ abortIfNeeded(context2.signal);
352247
+ const { sendEvent, sessionId, messageId } = context2;
352248
+ sendEvent(sessionId, {
352249
+ messageId,
352250
+ type: "sub_tool_started",
352251
+ toolName: "spawn_subagents",
352252
+ title: `Fan out ${args.tasks.length} sub-agent(s)`,
352253
+ hint: `concurrency ${args.maxConcurrent ?? 3}`,
352254
+ input: JSON.stringify(args)
352255
+ });
352256
+ const finish = (output) => {
352257
+ sendEvent(sessionId, { messageId, type: "sub_tool_delta", outputDelta: output });
352258
+ sendEvent(sessionId, { messageId, type: "sub_tool_finished" });
352259
+ return output;
352260
+ };
352261
+ try {
352262
+ const { validateSubAgentSpec: validateSubAgentSpec2, runSubAgents: runSubAgents2, renderSubAgentSummary: renderSubAgentSummary2 } = await Promise.resolve().then(() => (init_subAgent(), subAgent_exports));
352263
+ const validation = validateSubAgentSpec2(args);
352264
+ if (!validation.ok) {
352265
+ return finish(`Invalid sub-agent spec: ${validation.error}`);
352266
+ }
352267
+ const agentService = context2.agentService;
352268
+ if (!agentService?.startTask) {
352269
+ return finish(
352270
+ "Sub-agent delegation is not available in this runtime (no agent service bridge)."
352271
+ );
352272
+ }
352273
+ const summary = await runSubAgents2(
352274
+ {
352275
+ tasks: args.tasks.map((t) => ({ label: t.label, prompt: t.prompt })),
352276
+ maxConcurrent: args.maxConcurrent,
352277
+ timeoutMs: args.timeoutMs
352278
+ },
352279
+ async (task2) => {
352280
+ const childSession = await agentService.createSession();
352281
+ const result = await agentService.startTask({
352282
+ sessionId: childSession.sessionId,
352283
+ userInput: task2.prompt
352284
+ });
352285
+ const text = typeof result === "string" ? result : String(result?.answer ?? result?.output ?? result);
352286
+ return text;
352287
+ }
352288
+ );
352289
+ return finish(renderSubAgentSummary2(summary));
352290
+ } catch (error40) {
352291
+ if (error40 instanceof Error && error40.name === "AbortError") throw error40;
352292
+ return finish(`Sub-agent fan-out failed: ${error40 instanceof Error ? error40.message : String(error40)}`);
352293
+ }
352294
+ }
352295
+
351872
352296
  // ../../packages/backend/src/services/AgentHelper/tools/wait_tools.ts
351873
352297
  init_zod();
351874
352298
  var waitSchema = external_exports.object({
@@ -354243,6 +354667,11 @@ var CORE_METHODS = [
354243
354667
  m("gateway:isSameMachine", "gateway", "Whether the caller is on the same machine as the backend.", "1.0.0"),
354244
354668
  m("gateway:createSession", "session", "Create an agent session (returns sessionId).", "1.0.0"),
354245
354669
  m("session:list", "session", "List chat sessions.", "1.0.0"),
354670
+ m("history:search", "session", "Full-text search across ALL stored chat sessions (v3.2.18). Params: {query, wholeWord?, includeTitles?, sessionLimit?, snippetLimit?}. Returns matching sessions ranked by match count with snippets.", "3.2.18"),
354671
+ m("settings:listBackups", "settings", "List timestamped settings backups (v3.2.18). Returns [{name, at, size}] newest-first.", "3.2.18"),
354672
+ m("settings:restoreBackup", "settings", "Restore settings from a named backup (v3.2.18). Takes a SAFETY backup of the current state first. Params: {name}.", "3.2.18"),
354673
+ m("settings:export", "settings", "Export the current settings.json content as a portable JSON string (v3.2.18).", "3.2.18"),
354674
+ m("settings:import", "settings", "Import settings JSON (v3.2.18). Validates it is a JSON object; takes a SAFETY backup before replacing. Params: {content}.", "3.2.18"),
354246
354675
  m("session:get", "session", "Get one chat session by id.", "1.0.0", { sessionId: { type: "string" } }),
354247
354676
  m("agent:startTask", "agent", "Start an AI agent task (blocking) and return the final answer.", "1.0.0", { sessionId: { type: "string" }, userInput: { type: "string" } }),
354248
354677
  m("agent:startTaskAsync", "agent", "Start an AI agent task (async; watch events for progress).", "1.0.0", { sessionId: { type: "string" }, userInput: { type: "string" } }),
@@ -355212,6 +355641,11 @@ function buildToolsForModel(readFileSupport) {
355212
355641
  description: BUILTIN_TOOL_INFO.find((t) => t.name === "close_terminal_tab")?.description ?? "",
355213
355642
  schema: closeTerminalTabSchema
355214
355643
  },
355644
+ {
355645
+ name: "spawn_subagents",
355646
+ description: BUILTIN_TOOL_INFO.find((t) => t.name === "spawn_subagents")?.description ?? "",
355647
+ schema: spawnSubAgentsSchema
355648
+ },
355215
355649
  {
355216
355650
  name: WRITE_FILE_TOOL_NAME,
355217
355651
  description: WRITE_FILE_TOOL_DESCRIPTION,
@@ -355450,6 +355884,7 @@ var toolImplementations = {
355450
355884
  reconnectTerminalTab,
355451
355885
  openTerminalTab,
355452
355886
  closeTerminalTab,
355887
+ spawnSubAgents,
355453
355888
  wait,
355454
355889
  waitTerminalIdle,
355455
355890
  copyBetweenTabs,
@@ -368370,7 +368805,7 @@ Missing image attachments (kept as references only): ${missingNames.join(", ")}`
368370
368805
  // ../../packages/backend/src/services/PassChatTempExportService.ts
368371
368806
  var import_node_fs4 = __toESM(require("node:fs"), 1);
368372
368807
  var import_promises6 = __toESM(require("node:fs/promises"), 1);
368373
- var import_node_os = __toESM(require("node:os"), 1);
368808
+ var import_node_os2 = __toESM(require("node:os"), 1);
368374
368809
  var import_node_path6 = __toESM(require("node:path"), 1);
368375
368810
  var import_node_crypto15 = __toESM(require("node:crypto"), 1);
368376
368811
  var DEFAULT_MAX_FILES = 50;
@@ -368379,7 +368814,7 @@ var PassChatTempExportService = class {
368379
368814
  maxFiles;
368380
368815
  groupBySession;
368381
368816
  constructor(options) {
368382
- this.baseDir = options?.baseDir || import_node_path6.default.join(import_node_os.default.tmpdir(), "gyshell-pass-chats");
368817
+ this.baseDir = options?.baseDir || import_node_path6.default.join(import_node_os2.default.tmpdir(), "gyshell-pass-chats");
368383
368818
  this.maxFiles = options?.maxFiles === null ? null : Number.isInteger(options?.maxFiles) && Number(options?.maxFiles) > 0 ? Number(options?.maxFiles) : DEFAULT_MAX_FILES;
368384
368819
  this.groupBySession = options?.groupBySession === true;
368385
368820
  }
@@ -369002,6 +369437,20 @@ var AgentService_v2 = class {
369002
369437
  isAbortError(error40) {
369003
369438
  return this.helpers.isAbortError(error40);
369004
369439
  }
369440
+ /**
369441
+ * v3.2.18: resolve a fallback model id to its ModelDefinition from the
369442
+ * session's settings. Returns null when the model isn't configured.
369443
+ */
369444
+ resolveFallbackModelItem(modelId) {
369445
+ try {
369446
+ const settings = this.options?.getSettings?.() ?? null;
369447
+ const items = settings?.models?.items ?? [];
369448
+ const hit = items.find((m2) => m2.model === modelId || m2.id === modelId || m2.name === modelId);
369449
+ return hit ?? null;
369450
+ } catch {
369451
+ return null;
369452
+ }
369453
+ }
369005
369454
  /**
369006
369455
  * v3.2.12: true when the given run's final_output node already emitted the
369007
369456
  * `done` event. GatewayService uses this to avoid broadcasting a duplicate
@@ -369549,172 +369998,228 @@ var AgentService_v2 = class {
369549
369998
  let reasoningContent = "";
369550
369999
  let debugRawChunks = [];
369551
370000
  const llmTraceStart = Date.now();
369552
- const fullResponse = await invokeWithRetryAndSanitizedInput({
369553
- helpers: this.helpers,
369554
- messages: modelInputMessages,
369555
- modelSupportsImage: sessionBinding.readFileSupport.image,
369556
- signal: config2?.signal,
369557
- operation: async (streamInputMessages) => {
369558
- const stream = await modelWithTools.stream(streamInputMessages, {
369559
- signal: config2?.signal
370001
+ const fallbacks = sessionBinding.fallbackModels;
370002
+ let fullResponse;
370003
+ if (Array.isArray(fallbacks) && fallbacks.length > 0) {
370004
+ const { withModelFailover: withModelFailover2 } = await Promise.resolve().then(() => (init_modelFailover(), modelFailover_exports));
370005
+ const primaryModel = baseModel?.model ?? baseModel?.modelName ?? "unknown";
370006
+ const chain = [{ model: primaryModel }, ...fallbacks.map((f) => ({ model: f.model, label: f.label }))];
370007
+ const failover = await withModelFailover2(chain, async (candidate) => {
370008
+ let modelToUse = modelWithTools;
370009
+ if (candidate.model !== primaryModel) {
370010
+ const fallbackItem = this.resolveFallbackModelItem(candidate.model);
370011
+ if (!fallbackItem) {
370012
+ throw new Error(`fallback model "${candidate.model}" is not configured`);
370013
+ }
370014
+ const fallbackBinding = this.getSessionModelBinding(sessionId);
370015
+ const fallbackChat = this.helpers.createChatModel(fallbackItem, 0.7, fallbackBinding?.profile ?? null);
370016
+ modelToUse = fallbackChat;
370017
+ }
370018
+ return await invokeWithRetryAndSanitizedInput({
370019
+ helpers: this.helpers,
370020
+ messages: modelInputMessages,
370021
+ modelSupportsImage: sessionBinding.readFileSupport.image,
370022
+ signal: config2?.signal,
370023
+ operation: async (streamInputMessages) => {
370024
+ const stream = await modelToUse.stream(streamInputMessages, { signal: config2?.signal });
370025
+ let response = null;
370026
+ for await (const chunk2 of stream) {
370027
+ response = response ? response.concat(chunk2) : chunk2;
370028
+ }
370029
+ if (!response) throw new Error("Model stream ended without a usable response.");
370030
+ return response;
370031
+ },
370032
+ onRetry: (attempt) => {
370033
+ this.helpers.sendEvent(sessionId, {
370034
+ type: "alert",
370035
+ message: `Retrying (${attempt}/${MODEL_RETRY_MAX})...`,
370036
+ level: "info",
370037
+ messageId: `retry-${messageId}-${attempt}`
370038
+ });
370039
+ },
370040
+ maxRetries: MODEL_RETRY_MAX,
370041
+ delaysMs: MODEL_RETRY_DELAYS_MS
369560
370042
  });
369561
- let response = null;
369562
- let skippedEmptyGenericChunks = 0;
369563
- const streamReasoningExtractor = createStreamReasoningExtractor();
369564
- const attemptDebugRawChunks = [];
369565
- let activeReasoningBannerId = null;
369566
- const startReasoningBanner = () => {
369567
- if (activeReasoningBannerId) return;
369568
- activeReasoningBannerId = v4_default();
369569
- this.helpers.sendEvent(sessionId, {
369570
- messageId: activeReasoningBannerId,
369571
- type: "sub_tool_started",
369572
- title: "Reasoning...",
369573
- hint: ""
369574
- });
369575
- };
369576
- const appendReasoningDelta = (delta) => {
369577
- if (!delta) return;
369578
- startReasoningBanner();
369579
- this.helpers.sendEvent(sessionId, {
369580
- messageId: activeReasoningBannerId,
369581
- type: "sub_tool_delta",
369582
- outputDelta: delta
369583
- });
369584
- };
369585
- const finishReasoningBanner = () => {
369586
- if (!activeReasoningBannerId) return;
369587
- this.helpers.sendEvent(sessionId, {
369588
- messageId: activeReasoningBannerId,
369589
- type: "sub_tool_finished"
370043
+ }, {
370044
+ onAttempt: (a) => {
370045
+ if (!a.ok) {
370046
+ console.warn(`[AgentService_v2] model failover: ${a.model} failed (${a.reason}) after ${a.durationMs}ms`);
370047
+ }
370048
+ }
370049
+ });
370050
+ if (failover.value !== void 0) {
370051
+ fullResponse = failover.value;
370052
+ } else {
370053
+ throw failover.error ?? new Error("model failover exhausted all candidates");
370054
+ }
370055
+ } else {
370056
+ fullResponse = await invokeWithRetryAndSanitizedInput({
370057
+ helpers: this.helpers,
370058
+ messages: modelInputMessages,
370059
+ modelSupportsImage: sessionBinding.readFileSupport.image,
370060
+ signal: config2?.signal,
370061
+ operation: async (streamInputMessages) => {
370062
+ const stream = await modelWithTools.stream(streamInputMessages, {
370063
+ signal: config2?.signal
369590
370064
  });
369591
- activeReasoningBannerId = null;
369592
- };
369593
- try {
369594
- for await (const chunk2 of stream) {
369595
- const rawChunk = captureRawResponseChunk(
369596
- chunk2,
369597
- attemptDebugRawChunks
369598
- );
369599
- const extracted = streamReasoningExtractor.processChunk(
369600
- chunk2,
369601
- rawChunk
369602
- );
369603
- const appendResult = appendStreamedModelResponseChunk(
369604
- response,
369605
- chunk2,
369606
- rawChunk
369607
- );
369608
- response = appendResult.response;
369609
- if (appendResult.skippedEmptyGenericChunk) {
369610
- skippedEmptyGenericChunks += 1;
369611
- }
369612
- const rawDelta = this.helpers.extractText(chunk2.content);
369613
- if (rawDelta) {
369614
- partialText += rawDelta;
369615
- }
369616
- if (extracted.reasoning) {
369617
- appendReasoningDelta(extracted.reasoning);
369618
- } else {
369619
- finishReasoningBanner();
370065
+ let response = null;
370066
+ let skippedEmptyGenericChunks = 0;
370067
+ const streamReasoningExtractor = createStreamReasoningExtractor();
370068
+ const attemptDebugRawChunks = [];
370069
+ let activeReasoningBannerId = null;
370070
+ const startReasoningBanner = () => {
370071
+ if (activeReasoningBannerId) return;
370072
+ activeReasoningBannerId = v4_default();
370073
+ this.helpers.sendEvent(sessionId, {
370074
+ messageId: activeReasoningBannerId,
370075
+ type: "sub_tool_started",
370076
+ title: "Reasoning...",
370077
+ hint: ""
370078
+ });
370079
+ };
370080
+ const appendReasoningDelta = (delta) => {
370081
+ if (!delta) return;
370082
+ startReasoningBanner();
370083
+ this.helpers.sendEvent(sessionId, {
370084
+ messageId: activeReasoningBannerId,
370085
+ type: "sub_tool_delta",
370086
+ outputDelta: delta
370087
+ });
370088
+ };
370089
+ const finishReasoningBanner = () => {
370090
+ if (!activeReasoningBannerId) return;
370091
+ this.helpers.sendEvent(sessionId, {
370092
+ messageId: activeReasoningBannerId,
370093
+ type: "sub_tool_finished"
370094
+ });
370095
+ activeReasoningBannerId = null;
370096
+ };
370097
+ try {
370098
+ for await (const chunk2 of stream) {
370099
+ const rawChunk = captureRawResponseChunk(
370100
+ chunk2,
370101
+ attemptDebugRawChunks
370102
+ );
370103
+ const extracted = streamReasoningExtractor.processChunk(
370104
+ chunk2,
370105
+ rawChunk
370106
+ );
370107
+ const appendResult = appendStreamedModelResponseChunk(
370108
+ response,
370109
+ chunk2,
370110
+ rawChunk
370111
+ );
370112
+ response = appendResult.response;
370113
+ if (appendResult.skippedEmptyGenericChunk) {
370114
+ skippedEmptyGenericChunks += 1;
370115
+ }
370116
+ const rawDelta = this.helpers.extractText(chunk2.content);
370117
+ if (rawDelta) {
370118
+ partialText += rawDelta;
370119
+ }
370120
+ if (extracted.reasoning) {
370121
+ appendReasoningDelta(extracted.reasoning);
370122
+ } else {
370123
+ finishReasoningBanner();
370124
+ }
370125
+ if (extracted.content) {
370126
+ this.helpers.sendEvent(sessionId, {
370127
+ messageId,
370128
+ type: "say",
370129
+ content: extracted.content
370130
+ });
370131
+ }
369620
370132
  }
369621
- if (extracted.content) {
370133
+ const pendingContent = streamReasoningExtractor.flushPendingContent();
370134
+ if (pendingContent) {
369622
370135
  this.helpers.sendEvent(sessionId, {
369623
370136
  messageId,
369624
370137
  type: "say",
369625
- content: extracted.content
370138
+ content: pendingContent
369626
370139
  });
369627
370140
  }
370141
+ finishReasoningBanner();
370142
+ } catch (err) {
370143
+ finishReasoningBanner();
370144
+ if (partialText.trim()) {
370145
+ this.lastAbortedMessage = new AIMessage({
370146
+ content: partialText,
370147
+ additional_kwargs: {
370148
+ _gyshellMessageId: messageId,
370149
+ _gyshellAborted: true
370150
+ }
370151
+ });
370152
+ console.log(
370153
+ "[AgentService_v2] Captured partial message from error/abort in instance variable."
370154
+ );
370155
+ }
370156
+ throw err;
369628
370157
  }
369629
- const pendingContent = streamReasoningExtractor.flushPendingContent();
369630
- if (pendingContent) {
369631
- this.helpers.sendEvent(sessionId, {
369632
- messageId,
369633
- type: "say",
369634
- content: pendingContent
369635
- });
370158
+ if (!response) {
370159
+ throw new Error("Model stream ended without a usable response.");
369636
370160
  }
369637
- finishReasoningBanner();
369638
- } catch (err) {
369639
- finishReasoningBanner();
369640
- if (partialText.trim()) {
369641
- this.lastAbortedMessage = new AIMessage({
369642
- content: partialText,
369643
- additional_kwargs: {
369644
- _gyshellMessageId: messageId,
369645
- _gyshellAborted: true
370161
+ if (skippedEmptyGenericChunks > 0) {
370162
+ response.additional_kwargs = {
370163
+ ...response.additional_kwargs || {},
370164
+ [SKIPPED_EMPTY_GENERIC_CHUNKS_KEY]: skippedEmptyGenericChunks
370165
+ };
370166
+ }
370167
+ reasoningContent = streamReasoningExtractor.getReasoningContent();
370168
+ if (isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && typeof modelWithTools.invoke === "function") {
370169
+ console.warn(
370170
+ `[AgentService_v2] Stream ended with malformed empty tool-call finish; retrying same request with non-stream invoke (sessionId=${sessionId}).`
370171
+ );
370172
+ const invokeResponse = await modelWithTools.invoke(
370173
+ streamInputMessages,
370174
+ {
370175
+ signal: config2?.signal
369646
370176
  }
369647
- });
369648
- console.log(
369649
- "[AgentService_v2] Captured partial message from error/abort in instance variable."
369650
370177
  );
369651
- }
369652
- throw err;
369653
- }
369654
- if (!response) {
369655
- throw new Error("Model stream ended without a usable response.");
369656
- }
369657
- if (skippedEmptyGenericChunks > 0) {
369658
- response.additional_kwargs = {
369659
- ...response.additional_kwargs || {},
369660
- [SKIPPED_EMPTY_GENERIC_CHUNKS_KEY]: skippedEmptyGenericChunks
369661
- };
369662
- }
369663
- reasoningContent = streamReasoningExtractor.getReasoningContent();
369664
- if (isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && typeof modelWithTools.invoke === "function") {
369665
- console.warn(
369666
- `[AgentService_v2] Stream ended with malformed empty tool-call finish; retrying same request with non-stream invoke (sessionId=${sessionId}).`
369667
- );
369668
- const invokeResponse = await modelWithTools.invoke(
369669
- streamInputMessages,
369670
- {
369671
- signal: config2?.signal
369672
- }
369673
- );
369674
- captureRawResponseChunk(
369675
- invokeResponse,
369676
- attemptDebugRawChunks
369677
- );
369678
- if (!isEmptyMalformedToolCallFinish(
369679
- invokeResponse,
369680
- attemptDebugRawChunks
369681
- )) {
369682
- const invokeText = this.helpers.extractText(
369683
- invokeResponse.content
370178
+ captureRawResponseChunk(
370179
+ invokeResponse,
370180
+ attemptDebugRawChunks
369684
370181
  );
369685
- if (invokeText) {
369686
- this.helpers.sendEvent(sessionId, {
369687
- messageId,
369688
- type: "say",
369689
- content: invokeText
369690
- });
370182
+ if (!isEmptyMalformedToolCallFinish(
370183
+ invokeResponse,
370184
+ attemptDebugRawChunks
370185
+ )) {
370186
+ const invokeText = this.helpers.extractText(
370187
+ invokeResponse.content
370188
+ );
370189
+ if (invokeText) {
370190
+ this.helpers.sendEvent(sessionId, {
370191
+ messageId,
370192
+ type: "say",
370193
+ content: invokeText
370194
+ });
370195
+ }
370196
+ response = invokeResponse;
369691
370197
  }
369692
- response = invokeResponse;
369693
370198
  }
369694
- }
369695
- if (!isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && isEmptyUnusableModelResponse(response, attemptDebugRawChunks)) {
369696
- const finishReason = describeStreamedResponseFinish(
369697
- response,
369698
- attemptDebugRawChunks
369699
- );
369700
- throw new Error(
369701
- `Model stream ended with an empty unusable response (finish_reason=${finishReason}).`
369702
- );
369703
- }
369704
- debugRawChunks = attemptDebugRawChunks;
369705
- return response;
369706
- },
369707
- onRetry: (attempt) => {
369708
- this.helpers.sendEvent(sessionId, {
369709
- type: "alert",
369710
- message: `Retrying (${attempt}/${MODEL_RETRY_MAX})...`,
369711
- level: "info",
369712
- messageId: `retry-${messageId}-${attempt}`
369713
- });
369714
- },
369715
- maxRetries: MODEL_RETRY_MAX,
369716
- delaysMs: MODEL_RETRY_DELAYS_MS
369717
- });
370199
+ if (!isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && isEmptyUnusableModelResponse(response, attemptDebugRawChunks)) {
370200
+ const finishReason = describeStreamedResponseFinish(
370201
+ response,
370202
+ attemptDebugRawChunks
370203
+ );
370204
+ throw new Error(
370205
+ `Model stream ended with an empty unusable response (finish_reason=${finishReason}).`
370206
+ );
370207
+ }
370208
+ debugRawChunks = attemptDebugRawChunks;
370209
+ return response;
370210
+ },
370211
+ onRetry: (attempt) => {
370212
+ this.helpers.sendEvent(sessionId, {
370213
+ type: "alert",
370214
+ message: `Retrying (${attempt}/${MODEL_RETRY_MAX})...`,
370215
+ level: "info",
370216
+ messageId: `retry-${messageId}-${attempt}`
370217
+ });
370218
+ },
370219
+ maxRetries: MODEL_RETRY_MAX,
370220
+ delaysMs: MODEL_RETRY_DELAYS_MS
370221
+ });
370222
+ }
369718
370223
  fullResponse.additional_kwargs = {
369719
370224
  ...fullResponse.additional_kwargs || {},
369720
370225
  _gyshellMessageId: messageId
@@ -370110,6 +370615,22 @@ Actually, your intention might be different. Please re-read the description of t
370110
370615
  }
370111
370616
  break;
370112
370617
  }
370618
+ case "spawn_subagents": {
370619
+ try {
370620
+ const validatedArgs = spawnSubAgentsSchema.parse(
370621
+ toolCall.args || {}
370622
+ );
370623
+ result = await toolImplementations.spawnSubAgents(
370624
+ validatedArgs,
370625
+ // The sub-agent tool needs the agent service to create child
370626
+ // sessions — inject it into the execution context.
370627
+ { ...executionContext, agentService: this }
370628
+ );
370629
+ } catch (err) {
370630
+ result = `Parameter validation error for spawn_subagents: ${err.message}`;
370631
+ }
370632
+ break;
370633
+ }
370113
370634
  case "manage_ssh_connection": {
370114
370635
  try {
370115
370636
  const validatedArgs = manageSshConnectionSchema.parse(
@@ -374652,13 +375173,16 @@ var WebSocketGatewayAdapter = class {
374652
375173
  }
374653
375174
  const token = this.extractAccessToken(request);
374654
375175
  if (!token) {
375176
+ this.options.rateLimiter?.recordAuthFailure(this.clientKeyForRequest(request));
374655
375177
  return "missing access token";
374656
375178
  }
374657
375179
  try {
374658
375180
  const valid = await auth2.verifyToken(token);
374659
375181
  if (!valid) {
375182
+ this.options.rateLimiter?.recordAuthFailure(this.clientKeyForRequest(request));
374660
375183
  return "invalid access token";
374661
375184
  }
375185
+ this.options.rateLimiter?.recordAuthSuccess(this.clientKeyForRequest(request));
374662
375186
  return null;
374663
375187
  } catch (error40) {
374664
375188
  this.logger.warn(
@@ -374668,6 +375192,15 @@ var WebSocketGatewayAdapter = class {
374668
375192
  return "token verification failed";
374669
375193
  }
374670
375194
  }
375195
+ /** v3.2.18: a stable per-client key for rate limiting (IP or token id). */
375196
+ clientKeyForRequest(request) {
375197
+ return String(request?.socket?.remoteAddress || "unknown");
375198
+ }
375199
+ /** v3.2.18: per-client key from a live socket connection. */
375200
+ clientKeyForSocket(socket) {
375201
+ const remote = socket.remoteAddress;
375202
+ return String(remote || "unknown");
375203
+ }
374671
375204
  extractAccessToken(request) {
374672
375205
  const fromAuthHeader = this.readBearerToken(
374673
375206
  request?.headers?.authorization
@@ -374828,6 +375361,17 @@ var WebSocketGatewayAdapter = class {
374828
375361
  try {
374829
375362
  const parsed = this.parseRequest(raw);
374830
375363
  requestId = parsed.id !== void 0 ? String(parsed.id) : void 0;
375364
+ const limiter = this.options.rateLimiter;
375365
+ if (limiter) {
375366
+ const clientKey = this.clientKeyForSocket(socket);
375367
+ const decision = limiter.check(clientKey);
375368
+ if (!decision.allowed) {
375369
+ throw new WebSocketRpcError(
375370
+ "RATE_LIMITED",
375371
+ decision.reason === "locked-out" ? `locked out; retry in ${Math.ceil((decision.retryAfterMs ?? 0) / 1e3)}s` : `rate limited; retry in ${Math.ceil((decision.retryAfterMs ?? 1e3) / 1e3)}s`
375372
+ );
375373
+ }
375374
+ }
374831
375375
  const result = await this.executeRequest(parsed, socket);
374832
375376
  if (requestId) {
374833
375377
  this.sendRpcSuccess(socket, requestId, result);
@@ -374885,6 +375429,26 @@ var WebSocketGatewayAdapter = class {
374885
375429
  "Unsupported websocket payload type."
374886
375430
  );
374887
375431
  }
375432
+ /**
375433
+ * v3.2.18: public dispatch for non-WebSocket callers (the REST API layer).
375434
+ * Executes a gateway method with a synthetic loopback socket — same
375435
+ * validation and error mapping as a WS request, no socket required.
375436
+ */
375437
+ async handleRequest(method, params) {
375438
+ const syntheticSocket = {
375439
+ send: () => {
375440
+ },
375441
+ close: () => {
375442
+ },
375443
+ on: () => {
375444
+ },
375445
+ readyState: 1
375446
+ };
375447
+ return await this.executeRequest(
375448
+ { id: `rest-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, method, params },
375449
+ syntheticSocket
375450
+ );
375451
+ }
374888
375452
  async executeRequest(request, socket) {
374889
375453
  const params = request.params ?? {};
374890
375454
  if (request.method.startsWith("observability:")) {
@@ -374961,6 +375525,61 @@ var WebSocketGatewayAdapter = class {
374961
375525
  }
374962
375526
  return { session };
374963
375527
  }
375528
+ case "history:search": {
375529
+ const bridge = this.options.historyBridge;
375530
+ if (!bridge?.getAllSessions) {
375531
+ throw new WebSocketRpcError(
375532
+ "METHOD_NOT_FOUND",
375533
+ "history:search is not available on this gateway (no history bridge)."
375534
+ );
375535
+ }
375536
+ const { searchChatHistory: searchChatHistory2 } = await Promise.resolve().then(() => (init_historySearch(), historySearch_exports));
375537
+ const sessions = await bridge.getAllSessions();
375538
+ const query = this.readStringParam(params, "query");
375539
+ const wholeWord = params?.wholeWord === true;
375540
+ const includeTitles = params?.includeTitles !== false;
375541
+ const sessionLimit = typeof params?.sessionLimit === "number" ? params.sessionLimit : void 0;
375542
+ const snippetLimit = typeof params?.snippetLimit === "number" ? params.snippetLimit : void 0;
375543
+ return searchChatHistory2(sessions, query, { wholeWord, includeTitles, sessionLimit, snippetLimit });
375544
+ }
375545
+ case "settings:listBackups": {
375546
+ const bridge = this.options.settingsBridge;
375547
+ if (!bridge?.listBackups) {
375548
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings backups are not available on this gateway.");
375549
+ }
375550
+ return { backups: await bridge.listBackups() };
375551
+ }
375552
+ case "settings:restoreBackup": {
375553
+ const bridge = this.options.settingsBridge;
375554
+ if (!bridge?.restoreBackup) {
375555
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings backups are not available on this gateway.");
375556
+ }
375557
+ const name = this.readStringParam(params, "name");
375558
+ const result = await bridge.restoreBackup(name);
375559
+ if (result && typeof result === "object" && "ok" in result && !result.ok) {
375560
+ throw new WebSocketRpcError("BAD_REQUEST", String(result.error ?? "restore failed"));
375561
+ }
375562
+ return { restored: name };
375563
+ }
375564
+ case "settings:export": {
375565
+ const bridge = this.options.settingsBridge;
375566
+ if (!bridge?.export) {
375567
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings export is not available on this gateway.");
375568
+ }
375569
+ return { content: await bridge.export() };
375570
+ }
375571
+ case "settings:import": {
375572
+ const bridge = this.options.settingsBridge;
375573
+ if (!bridge?.import) {
375574
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings import is not available on this gateway.");
375575
+ }
375576
+ const content = this.readStringParam(params, "content");
375577
+ const result = await bridge.import(content);
375578
+ if (result && typeof result === "object" && "ok" in result && !result.ok) {
375579
+ throw new WebSocketRpcError("BAD_REQUEST", String(result.error ?? "import failed"));
375580
+ }
375581
+ return { imported: true };
375582
+ }
374964
375583
  case "agent:exportHistory": {
374965
375584
  const bridge = this.options.agentBridge;
374966
375585
  if (!bridge?.exportHistory) {
@@ -383611,7 +384230,7 @@ var NodeMcpToolService = class extends McpRuntimeCore {
383611
384230
 
383612
384231
  // ../../packages/backend/src/adapters/node/NodeSkillService.ts
383613
384232
  var import_node_path16 = __toESM(require("node:path"), 1);
383614
- var import_node_os3 = __toESM(require("node:os"), 1);
384233
+ var import_node_os4 = __toESM(require("node:os"), 1);
383615
384234
 
383616
384235
  // ../../packages/backend/src/skills/FileSkillStore.ts
383617
384236
  var import_promises9 = __toESM(require("node:fs/promises"), 1);
@@ -383916,10 +384535,10 @@ ${files}`;
383916
384535
 
383917
384536
  // ../../packages/backend/src/skills/scanRoots.ts
383918
384537
  var import_node_path15 = __toESM(require("node:path"), 1);
383919
- var import_node_os2 = __toESM(require("node:os"), 1);
384538
+ var import_node_os3 = __toESM(require("node:os"), 1);
383920
384539
  function resolveDefaultSkillScanRoots(options) {
383921
384540
  const primaryRoot = import_node_path15.default.resolve(options.primaryRoot);
383922
- const homeDir = (options.homeDir || import_node_os2.default.homedir() || "").trim();
384541
+ const homeDir = (options.homeDir || import_node_os3.default.homedir() || "").trim();
383923
384542
  const platform2 = options.platform || process.platform;
383924
384543
  const appData = (options.appData || process.env.APPDATA || "").trim();
383925
384544
  const codexHome = (options.codexHome || process.env.CODEX_HOME || "").trim();
@@ -383991,7 +384610,7 @@ var NodeSkillService = class {
383991
384610
  resolveScanRoots() {
383992
384611
  return resolveDefaultSkillScanRoots({
383993
384612
  primaryRoot: this.skillsDir,
383994
- homeDir: import_node_os3.default.homedir(),
384613
+ homeDir: import_node_os4.default.homedir(),
383995
384614
  platform: process.platform,
383996
384615
  appData: process.env.APPDATA,
383997
384616
  codexHome: process.env.CODEX_HOME
@@ -386114,6 +386733,367 @@ var SessionLogService = class _SessionLogService {
386114
386733
 
386115
386734
  // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
386116
386735
  init_schedulerService();
386736
+
386737
+ // ../../packages/backend/src/services/settings/settingsBackup.ts
386738
+ var BACKUP_PREFIX = "settings-backup-";
386739
+ var BACKUP_SUFFIX = ".json";
386740
+ function backupNameFor(date6) {
386741
+ const pad = (n2, w = 2) => String(n2).padStart(w, "0");
386742
+ return `${BACKUP_PREFIX}${date6.getUTCFullYear()}${pad(date6.getUTCMonth() + 1)}${pad(date6.getUTCDate())}-${pad(date6.getUTCHours())}${pad(date6.getUTCMinutes())}${pad(date6.getUTCSeconds())}${BACKUP_SUFFIX}`;
386743
+ }
386744
+ function parseBackupName(name) {
386745
+ if (!name.startsWith(BACKUP_PREFIX) || !name.endsWith(BACKUP_SUFFIX)) return null;
386746
+ const stem = name.slice(BACKUP_PREFIX.length, -BACKUP_SUFFIX.length);
386747
+ const m2 = stem.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/);
386748
+ if (!m2) return null;
386749
+ const [, y, mo, d, h, mi, s] = m2.map(Number);
386750
+ const date6 = new Date(Date.UTC(y, mo - 1, d, h, mi, s));
386751
+ return Number.isNaN(date6.getTime()) ? null : date6;
386752
+ }
386753
+ var SettingsBackupService = class {
386754
+ constructor(io, opts = {}) {
386755
+ this.io = io;
386756
+ this.keep = opts.keep ?? 20;
386757
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
386758
+ }
386759
+ keep;
386760
+ now;
386761
+ /** Take a backup of the CURRENT settings content, then rotate. */
386762
+ backup() {
386763
+ const content = this.io.read();
386764
+ const name = backupNameFor(this.now());
386765
+ this.io.writeBackup(name, content);
386766
+ this.rotate();
386767
+ return { name, at: this.now(), size: content.length };
386768
+ }
386769
+ /** Delete oldest backups beyond the keep limit. Returns names removed. */
386770
+ rotate() {
386771
+ const names = this.io.listBackups().map((n2) => ({ n: n2, at: parseBackupName(n2) })).filter((x) => x.at !== null).sort((a, b) => a.at.getTime() - b.at.getTime());
386772
+ const excess = names.slice(0, Math.max(0, names.length - this.keep));
386773
+ for (const { n: n2 } of excess) this.io.deleteBackup(n2);
386774
+ return excess.map((x) => x.n);
386775
+ }
386776
+ /** List backups, newest first. */
386777
+ list() {
386778
+ return this.io.listBackups().map((n2) => ({ n: n2, at: parseBackupName(n2) })).filter((x) => x.at !== null).sort((a, b) => b.at.getTime() - a.at.getTime()).map(({ n: n2, at }) => ({ name: n2, at, size: this.io.readBackup(n2).length }));
386779
+ }
386780
+ /** Restore a backup by name (writes it to the live settings path). */
386781
+ restore(name) {
386782
+ if (!this.io.listBackups().includes(name)) {
386783
+ return { ok: false, error: `no backup named "${name}"` };
386784
+ }
386785
+ this.backup();
386786
+ const content = this.io.readBackup(name);
386787
+ this.io.write(content);
386788
+ return { ok: true };
386789
+ }
386790
+ /** Export the current settings as a portable JSON string. */
386791
+ export() {
386792
+ return this.io.read();
386793
+ }
386794
+ /** Validate + import settings JSON. Refuses non-object / empty content. */
386795
+ import(content) {
386796
+ const trimmed = (content ?? "").trim();
386797
+ if (!trimmed) return { ok: false, error: "empty settings content" };
386798
+ let parsed;
386799
+ try {
386800
+ parsed = JSON.parse(trimmed);
386801
+ } catch (e) {
386802
+ return { ok: false, error: `invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
386803
+ }
386804
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
386805
+ return { ok: false, error: "settings content must be a JSON object" };
386806
+ }
386807
+ this.backup();
386808
+ this.io.write(trimmed);
386809
+ return { ok: true };
386810
+ }
386811
+ };
386812
+
386813
+ // ../../packages/backend/src/services/terminal/idleTimeout.ts
386814
+ var IdleTimeoutService = class {
386815
+ idleMs;
386816
+ protectedIds;
386817
+ now;
386818
+ lastActivity = /* @__PURE__ */ new Map();
386819
+ constructor(opts = {}) {
386820
+ this.idleMs = Math.max(0, (opts.idleMinutes ?? 30) * 6e4);
386821
+ this.protectedIds = new Set(opts.protectedIds ?? []);
386822
+ this.now = opts.now ?? (() => Date.now());
386823
+ }
386824
+ /** Record activity for a terminal. */
386825
+ touch(terminalId) {
386826
+ this.lastActivity.set(terminalId, this.now());
386827
+ }
386828
+ /** Register a terminal (starts its idle clock). */
386829
+ register(terminalId) {
386830
+ if (!this.lastActivity.has(terminalId)) {
386831
+ this.lastActivity.set(terminalId, this.now());
386832
+ }
386833
+ }
386834
+ /** Forget a terminal (on close). */
386835
+ forget(terminalId) {
386836
+ this.lastActivity.delete(terminalId);
386837
+ }
386838
+ /** Is this terminal idle past the threshold? */
386839
+ isIdle(terminalId) {
386840
+ if (this.protectedIds.has(terminalId)) return false;
386841
+ const last = this.lastActivity.get(terminalId);
386842
+ if (last === void 0) return false;
386843
+ return this.now() - last >= this.idleMs;
386844
+ }
386845
+ /** All currently-idle terminals (among the known ones). */
386846
+ idleTerminals(knownIds) {
386847
+ const t = this.now();
386848
+ const out = [];
386849
+ for (const id of knownIds) {
386850
+ if (this.protectedIds.has(id)) continue;
386851
+ const last = this.lastActivity.get(id);
386852
+ if (last === void 0) continue;
386853
+ const idleMin = (t - last) / 6e4;
386854
+ if (idleMin * 6e4 >= this.idleMs) {
386855
+ out.push({ terminalId: id, idleMinutes: Math.round(idleMin * 10) / 10 });
386856
+ }
386857
+ }
386858
+ return out;
386859
+ }
386860
+ /** Configure the threshold at runtime. */
386861
+ setIdleMinutes(minutes) {
386862
+ this.idleMs = Math.max(0, minutes * 6e4);
386863
+ }
386864
+ get idleMinutes() {
386865
+ return this.idleMs / 6e4;
386866
+ }
386867
+ };
386868
+
386869
+ // ../../packages/backend/src/services/Gateway/restApi.ts
386870
+ function matchRestRoute(routes, method, path32) {
386871
+ const normalized = path32.replace(/\/+$/, "") || "/";
386872
+ for (const route of routes) {
386873
+ if (route.method !== method.toUpperCase()) continue;
386874
+ const patternParts = route.path.split("/").filter(Boolean);
386875
+ const pathParts = normalized.split("/").filter(Boolean);
386876
+ if (patternParts.length !== pathParts.length) continue;
386877
+ const params = {};
386878
+ let matched = true;
386879
+ for (let i = 0; i < patternParts.length; i++) {
386880
+ const p = patternParts[i];
386881
+ if (p.startsWith(":")) {
386882
+ params[p.slice(1)] = decodeURIComponent(pathParts[i]);
386883
+ } else if (p !== pathParts[i]) {
386884
+ matched = false;
386885
+ break;
386886
+ }
386887
+ }
386888
+ if (matched) return { route, pathParams: params };
386889
+ }
386890
+ return null;
386891
+ }
386892
+ function defaultRestRoutes() {
386893
+ return [
386894
+ {
386895
+ method: "GET",
386896
+ path: "/api/v1/health",
386897
+ gatewayMethod: "gateway:ping",
386898
+ description: "Liveness check"
386899
+ },
386900
+ {
386901
+ method: "GET",
386902
+ path: "/api/v1/methods",
386903
+ gatewayMethod: "gateway:describe",
386904
+ description: "List all gateway methods (self-describing)"
386905
+ },
386906
+ {
386907
+ method: "GET",
386908
+ path: "/api/v1/terminals",
386909
+ gatewayMethod: "terminal:list",
386910
+ description: "List terminal tabs"
386911
+ },
386912
+ {
386913
+ method: "GET",
386914
+ path: "/api/v1/sessions",
386915
+ gatewayMethod: "session:list",
386916
+ description: "List chat sessions"
386917
+ },
386918
+ {
386919
+ method: "GET",
386920
+ path: "/api/v1/skills",
386921
+ gatewayMethod: "skills:getAll",
386922
+ description: "List loaded skills"
386923
+ },
386924
+ {
386925
+ method: "GET",
386926
+ path: "/api/v1/observability/metrics",
386927
+ gatewayMethod: "observability:metricsPrometheus",
386928
+ description: "Host metrics (Prometheus text or summary)"
386929
+ },
386930
+ {
386931
+ method: "GET",
386932
+ path: "/api/v1/observability/dashboard",
386933
+ gatewayMethod: "observability:liveDashboardState",
386934
+ description: "Live dashboard state"
386935
+ },
386936
+ {
386937
+ method: "GET",
386938
+ path: "/api/v1/observability/apm",
386939
+ gatewayMethod: "observability:apmSummary",
386940
+ description: "APM summary (LLM + app traces)"
386941
+ },
386942
+ {
386943
+ method: "GET",
386944
+ path: "/api/v1/history/search",
386945
+ gatewayMethod: "history:search",
386946
+ description: "Cross-session history search (?q=...)",
386947
+ buildParams: (_p, body) => {
386948
+ const b = body ?? {};
386949
+ return { query: b.q ?? b.query ?? "" };
386950
+ }
386951
+ },
386952
+ {
386953
+ method: "POST",
386954
+ path: "/api/v1/terminals/:id/write",
386955
+ gatewayMethod: "terminal:write",
386956
+ description: "Write data to a terminal tab",
386957
+ buildParams: (p, body) => {
386958
+ const b = body ?? {};
386959
+ return { terminalId: p.id, data: b.data ?? "" };
386960
+ }
386961
+ },
386962
+ {
386963
+ method: "GET",
386964
+ path: "/api/v1/terminals/:id/buffer",
386965
+ gatewayMethod: "terminal:getBufferDelta",
386966
+ description: "Read a terminal tab output delta",
386967
+ buildParams: (p, body) => {
386968
+ const b = body ?? {};
386969
+ return { terminalId: p.id, fromOffset: b.fromOffset ?? 0 };
386970
+ }
386971
+ },
386972
+ {
386973
+ method: "POST",
386974
+ path: "/api/v1/sessions/:id/chat",
386975
+ gatewayMethod: "agent:startTask",
386976
+ description: "Send a message to the agent (blocking)",
386977
+ buildParams: (p, body) => {
386978
+ const b = body ?? {};
386979
+ return { sessionId: p.id, userInput: b.message ?? b.userInput ?? "" };
386980
+ }
386981
+ },
386982
+ {
386983
+ method: "POST",
386984
+ path: "/api/v1/rpc",
386985
+ gatewayMethod: "",
386986
+ description: "Escape hatch: dispatch any gateway method",
386987
+ buildParams: (_p, body) => {
386988
+ const b = body ?? {};
386989
+ return { __rpcMethod: b.method ?? "", ...b.params ?? {} };
386990
+ }
386991
+ }
386992
+ ];
386993
+ }
386994
+ async function handleRestRequest(routes, dispatch, req) {
386995
+ const match = matchRestRoute(routes, req.method, req.path);
386996
+ if (!match) {
386997
+ return {
386998
+ status: 404,
386999
+ body: { error: "not_found", message: `No REST route for ${req.method} ${req.path}` }
387000
+ };
387001
+ }
387002
+ let gatewayMethod = match.route.gatewayMethod;
387003
+ let params;
387004
+ if (gatewayMethod === "") {
387005
+ const raw = req.body ?? {};
387006
+ if (!raw.method) {
387007
+ return { status: 400, body: { error: "bad_request", message: 'POST /api/v1/rpc needs {"method": "...", "params": {...}}' } };
387008
+ }
387009
+ gatewayMethod = raw.method;
387010
+ params = raw.params ?? {};
387011
+ } else {
387012
+ params = match.route.buildParams?.(match.pathParams, req.body) ?? match.pathParams;
387013
+ }
387014
+ try {
387015
+ const result = await dispatch(gatewayMethod, params);
387016
+ return { status: 200, body: result ?? { ok: true } };
387017
+ } catch (error40) {
387018
+ const message = error40 instanceof Error ? error40.message : String(error40);
387019
+ const status = /not found|no terminal|no session|no playbook/i.test(message) ? 404 : /invalid|bad request|must be|requires/i.test(message) ? 400 : 500;
387020
+ return { status, body: { error: "gateway_error", message } };
387021
+ }
387022
+ }
387023
+
387024
+ // ../../packages/backend/src/services/Gateway/gatewayRateLimit.ts
387025
+ var GatewayRateLimiter = class {
387026
+ capacity;
387027
+ refillPerSecond;
387028
+ authFailureLimit;
387029
+ authLockoutMs;
387030
+ now;
387031
+ buckets = /* @__PURE__ */ new Map();
387032
+ authTrackers = /* @__PURE__ */ new Map();
387033
+ constructor(opts = {}) {
387034
+ this.capacity = opts.capacity ?? 60;
387035
+ this.refillPerSecond = opts.refillPerSecond ?? 1;
387036
+ this.authFailureLimit = opts.authFailureLimit ?? 5;
387037
+ this.authLockoutMs = opts.authLockoutMs ?? 6e4;
387038
+ this.now = opts.now ?? (() => Date.now());
387039
+ }
387040
+ refill(bucket) {
387041
+ const t = this.now();
387042
+ const elapsed2 = (t - bucket.lastRefill) / 1e3;
387043
+ if (elapsed2 <= 0) return;
387044
+ bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed2 * this.refillPerSecond);
387045
+ bucket.lastRefill = t;
387046
+ }
387047
+ /** Check (and consume) one token for a client. */
387048
+ check(clientKey) {
387049
+ const t = this.now();
387050
+ const bucket = this.buckets.get(clientKey) ?? { tokens: this.capacity, lastRefill: t };
387051
+ this.refill(bucket);
387052
+ this.buckets.set(clientKey, bucket);
387053
+ if (bucket.tokens >= 1) {
387054
+ bucket.tokens -= 1;
387055
+ return { allowed: true, remaining: Math.floor(bucket.tokens) };
387056
+ }
387057
+ const msToNextToken = Math.ceil((1 - bucket.tokens) / this.refillPerSecond * 1e3);
387058
+ return { allowed: false, remaining: 0, retryAfterMs: msToNextToken, reason: "rate-limited" };
387059
+ }
387060
+ /** Record a failed authentication attempt; returns the lockout decision. */
387061
+ recordAuthFailure(clientKey) {
387062
+ const t = this.now();
387063
+ const tracker = this.authTrackers.get(clientKey) ?? { failures: 0, lockedUntil: 0 };
387064
+ tracker.failures += 1;
387065
+ if (tracker.failures >= this.authFailureLimit) {
387066
+ tracker.lockedUntil = t + this.authLockoutMs;
387067
+ tracker.failures = 0;
387068
+ }
387069
+ this.authTrackers.set(clientKey, tracker);
387070
+ if (t < tracker.lockedUntil) {
387071
+ return { allowed: false, remaining: 0, retryAfterMs: tracker.lockedUntil - t, reason: "locked-out" };
387072
+ }
387073
+ return { allowed: true, remaining: this.authFailureLimit - tracker.failures };
387074
+ }
387075
+ /** Clear auth failures on successful authentication. */
387076
+ recordAuthSuccess(clientKey) {
387077
+ this.authTrackers.delete(clientKey);
387078
+ }
387079
+ /** Is this client currently locked out? */
387080
+ isLockedOut(clientKey) {
387081
+ const tracker = this.authTrackers.get(clientKey);
387082
+ if (!tracker) return false;
387083
+ return this.now() < tracker.lockedUntil;
387084
+ }
387085
+ /** Drop state for a client (on disconnect). */
387086
+ forget(clientKey) {
387087
+ this.buckets.delete(clientKey);
387088
+ this.authTrackers.delete(clientKey);
387089
+ }
387090
+ /** Number of tracked clients (for monitoring/tests). */
387091
+ get clientCount() {
387092
+ return this.buckets.size;
387093
+ }
387094
+ };
387095
+
387096
+ // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
386117
387097
  init_scheduledTaskRunner();
386118
387098
 
386119
387099
  // ../../packages/backend/src/services/history/HistoryStorageMigration.ts
@@ -393608,11 +394588,11 @@ function createObservability(deps) {
393608
394588
  const policyEngine = new AgtPolicyEngine({
393609
394589
  loadPolicy: async () => {
393610
394590
  const req = (0, import_node_module5.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta7.url);
393611
- const fs22 = req("node:fs");
394591
+ const fs23 = req("node:fs");
393612
394592
  const path32 = req("node:path");
393613
394593
  const policyPath = path32.join(".", "policy.yaml");
393614
- if (fs22.existsSync(policyPath)) {
393615
- return parsePolicyYaml(fs22.readFileSync(policyPath, "utf8"));
394594
+ if (fs23.existsSync(policyPath)) {
394595
+ return parsePolicyYaml(fs23.readFileSync(policyPath, "utf8"));
393616
394596
  }
393617
394597
  return {
393618
394598
  name: "rterm-default",
@@ -393654,18 +394634,18 @@ function createObservability(deps) {
393654
394634
  const scanRoots = [pluginScanRoot, "./plugins"];
393655
394635
  try {
393656
394636
  const req = (0, import_node_module5.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta7.url);
393657
- const fs22 = req("node:fs");
393658
- if (fs22.existsSync(bundlePluginRoot)) scanRoots.push(bundlePluginRoot);
394637
+ const fs23 = req("node:fs");
394638
+ if (fs23.existsSync(bundlePluginRoot)) scanRoots.push(bundlePluginRoot);
393659
394639
  } catch {
393660
394640
  }
393661
394641
  try {
393662
394642
  const req = (0, import_node_module5.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta7.url);
393663
- const fs22 = req("node:fs");
394643
+ const fs23 = req("node:fs");
393664
394644
  const path32 = req("node:path");
393665
394645
  const resourcesPath = process.resourcesPath;
393666
394646
  if (resourcesPath) {
393667
394647
  const resourcesPlugins = path32.join(resourcesPath, "plugins");
393668
- if (fs22.existsSync(resourcesPlugins)) scanRoots.push(resourcesPlugins);
394648
+ if (fs23.existsSync(resourcesPlugins)) scanRoots.push(resourcesPlugins);
393669
394649
  }
393670
394650
  } catch {
393671
394651
  }
@@ -396631,434 +397611,581 @@ async function startGyBackend() {
396631
397611
  gatewayService.broadcastRaw("skills:updated", result.skills);
396632
397612
  gatewayService.broadcastRaw("memory:updated", result.memory);
396633
397613
  };
396634
- const wsGatewayControlService = new WebSocketGatewayControlService({
396635
- createAdapter: (host, port, ipFilter) => new WebSocketGatewayAdapter(gatewayService, {
396636
- host,
396637
- port,
396638
- accessTokenAuth: {
396639
- verifyToken: (token) => accessTokenService.verifyToken(token),
396640
- allowLocalhostWithoutToken: true
397614
+ const gatewayRateLimiter = new GatewayRateLimiter({
397615
+ capacity: Number(import_node_process2.default.env.RTERM_RATE_LIMIT_BURST ?? 60) || 60,
397616
+ refillPerSecond: Number(import_node_process2.default.env.RTERM_RATE_LIMIT_REFILL ?? 1) || 1,
397617
+ authFailureLimit: 5,
397618
+ authLockoutMs: 6e4
397619
+ });
397620
+ const settingsDir = import_node_path28.default.dirname(settingsService.getSettingsPath?.() ?? "");
397621
+ const backupsDir = settingsDir ? import_node_path28.default.join(settingsDir, "backups") : "";
397622
+ const settingsBackupService = new SettingsBackupService(
397623
+ backupsDir ? {
397624
+ read: () => import_node_fs12.default.readFileSync(settingsService.getSettingsPath(), "utf8"),
397625
+ write: (c) => import_node_fs12.default.writeFileSync(settingsService.getSettingsPath(), c),
397626
+ listBackups: () => import_node_fs12.default.existsSync(backupsDir) ? import_node_fs12.default.readdirSync(backupsDir) : [],
397627
+ readBackup: (n2) => import_node_fs12.default.readFileSync(import_node_path28.default.join(backupsDir, n2), "utf8"),
397628
+ writeBackup: (n2, c) => {
397629
+ import_node_fs12.default.mkdirSync(backupsDir, { recursive: true });
397630
+ import_node_fs12.default.writeFileSync(import_node_path28.default.join(backupsDir, n2), c);
396641
397631
  },
396642
- ipFilter,
396643
- // Browser dashboard on the SAME port as the WS gateway: /dashboard serves
396644
- // a live page (WS push via observability:liveDashboardSubscribe, with a
396645
- // /dashboard/json polling fallback). Auth mirrors the WS gateway —
396646
- // loopback open, remote needs a valid access token.
396647
- httpRoutes: [
396648
- {
396649
- path: "/dashboard",
396650
- handler: async (req, res) => {
396651
- if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
396652
- res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
396653
- res.end("missing/invalid access token");
396654
- return;
397632
+ deleteBackup: (n2) => {
397633
+ const p = import_node_path28.default.join(backupsDir, n2);
397634
+ if (import_node_fs12.default.existsSync(p)) import_node_fs12.default.unlinkSync(p);
397635
+ }
397636
+ } : { read: () => "", write: () => {
397637
+ }, listBackups: () => [], readBackup: () => "", writeBackup: () => {
397638
+ }, deleteBackup: () => {
397639
+ } },
397640
+ { keep: 20 }
397641
+ );
397642
+ const originalSetSettings = settingsService.setSettings.bind(settingsService);
397643
+ settingsService.setSettings = ((next) => {
397644
+ try {
397645
+ settingsBackupService.backup();
397646
+ } catch {
397647
+ }
397648
+ return originalSetSettings(next);
397649
+ });
397650
+ const idleTimeoutService = new IdleTimeoutService({
397651
+ idleMinutes: Number(import_node_process2.default.env.RTERM_IDLE_TIMEOUT_MINUTES ?? 30) || 30,
397652
+ protectedIds: [import_node_process2.default.env.GYBACKEND_TERMINAL_ID || "local-main"]
397653
+ });
397654
+ for (const t of terminalService.getAllTerminals()) idleTimeoutService.register(t.id);
397655
+ terminalService.onTerminalCreated?.((id) => idleTimeoutService.register(id));
397656
+ terminalService.onTerminalData?.((id) => idleTimeoutService.touch(id));
397657
+ terminalService.onTerminalClosed?.((id) => idleTimeoutService.forget(id));
397658
+ const idleSweeper = setInterval(() => {
397659
+ try {
397660
+ const idle = idleTimeoutService.idleTerminals(terminalService.getAllTerminals().map((t) => t.id));
397661
+ for (const { terminalId, idleMinutes } of idle) {
397662
+ console.log(`[gybackend] closing idle terminal ${terminalId} (${idleMinutes}min idle)`);
397663
+ try {
397664
+ terminalService.kill(terminalId);
397665
+ } catch {
397666
+ }
397667
+ idleTimeoutService.forget(terminalId);
397668
+ }
397669
+ } catch {
397670
+ }
397671
+ }, 6e4);
397672
+ if (typeof idleSweeper.unref === "function") idleSweeper.unref();
397673
+ let restDispatchTarget = null;
397674
+ const restDispatch = async (method, params) => {
397675
+ const target = restDispatchTarget;
397676
+ if (!target?.handleRequest) {
397677
+ throw new Error("REST API is not ready yet (gateway still starting)");
397678
+ }
397679
+ return await target.handleRequest(method, params);
397680
+ };
397681
+ const buildRestHttpRoutesSync = (opts) => {
397682
+ const routes = defaultRestRoutes();
397683
+ const staticRoutes = routes.filter((r) => !r.path.includes(":")).map((route) => ({
397684
+ path: route.path,
397685
+ handler: makeRestHandler(routes, opts)
397686
+ }));
397687
+ return staticRoutes;
397688
+ };
397689
+ const makeRestHandler = (routes, opts) => {
397690
+ return async (req, res) => {
397691
+ const r = req;
397692
+ const s = res;
397693
+ try {
397694
+ if (!await opts.isAuthorized(r)) {
397695
+ s.writeHead?.(401, { "content-type": "application/json" });
397696
+ s.end?.(JSON.stringify({ error: "unauthorized" }));
397697
+ return;
397698
+ }
397699
+ const url2 = new URL(r.url ?? "/", "http://localhost");
397700
+ const body = r.method === "POST" ? await readJsonBody(r) : Object.fromEntries(url2.searchParams.entries());
397701
+ const result = await handleRestRequest(routes, opts.dispatch, {
397702
+ method: r.method ?? "GET",
397703
+ path: url2.pathname,
397704
+ body
397705
+ });
397706
+ s.writeHead?.(result.status, { "content-type": "application/json" });
397707
+ s.end?.(JSON.stringify(result.body));
397708
+ } catch (e) {
397709
+ s.writeHead?.(500, { "content-type": "application/json" });
397710
+ s.end?.(JSON.stringify({ error: "internal", message: e instanceof Error ? e.message : String(e) }));
397711
+ }
397712
+ };
397713
+ };
397714
+ const readJsonBody = (req) => new Promise((resolve2) => {
397715
+ let data = "";
397716
+ req.on?.("data", (d) => {
397717
+ data += String(d ?? "");
397718
+ });
397719
+ req.on?.("end", () => {
397720
+ try {
397721
+ resolve2(data ? JSON.parse(data) : {});
397722
+ } catch {
397723
+ resolve2({});
397724
+ }
397725
+ });
397726
+ });
397727
+ const wsGatewayControlService = new WebSocketGatewayControlService({
397728
+ createAdapter: (host, port, ipFilter) => {
397729
+ const adapter = new WebSocketGatewayAdapter(gatewayService, {
397730
+ host,
397731
+ port,
397732
+ accessTokenAuth: {
397733
+ verifyToken: (token) => accessTokenService.verifyToken(token),
397734
+ allowLocalhostWithoutToken: true
397735
+ },
397736
+ // v3.2.18: per-client rate limiting (token bucket + auth lockout).
397737
+ rateLimiter: gatewayRateLimiter,
397738
+ ipFilter,
397739
+ // Browser dashboard on the SAME port as the WS gateway: /dashboard serves
397740
+ // a live page (WS push via observability:liveDashboardSubscribe, with a
397741
+ // /dashboard/json polling fallback). Auth mirrors the WS gateway —
397742
+ // loopback open, remote needs a valid access token.
397743
+ // v3.2.18: REST API routes (/api/v1/*) are prepended so curl/CI can
397744
+ // drive the gateway without a WS client.
397745
+ httpRoutes: [
397746
+ ...buildRestHttpRoutesSync({
397747
+ isAuthorized: (req) => dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t)),
397748
+ dispatch: restDispatch
397749
+ }),
397750
+ {
397751
+ path: "/dashboard",
397752
+ handler: async (req, res) => {
397753
+ if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
397754
+ res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
397755
+ res.end("missing/invalid access token");
397756
+ return;
397757
+ }
397758
+ const state = await observability.dashboard.state();
397759
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
397760
+ res.end(
397761
+ renderLiveDashboardHtml(state, {
397762
+ title: "RTerm \xB7 Unified Dashboard",
397763
+ dataUrl: "/dashboard/json"
397764
+ })
397765
+ );
397766
+ }
397767
+ },
397768
+ {
397769
+ path: "/dashboard/json",
397770
+ handler: async (req, res) => {
397771
+ if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
397772
+ res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
397773
+ res.end("missing/invalid access token");
397774
+ return;
397775
+ }
397776
+ const state = await observability.dashboard.state();
397777
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
397778
+ res.end(JSON.stringify(state));
396655
397779
  }
396656
- const state = await observability.dashboard.state();
396657
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
396658
- res.end(
396659
- renderLiveDashboardHtml(state, {
396660
- title: "RTerm \xB7 Unified Dashboard",
396661
- dataUrl: "/dashboard/json"
396662
- })
396663
- );
396664
397780
  }
396665
- },
396666
- {
396667
- path: "/dashboard/json",
396668
- handler: async (req, res) => {
396669
- if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
396670
- res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
396671
- res.end("missing/invalid access token");
396672
- return;
397781
+ ],
397782
+ terminalBridge: {
397783
+ listTerminals: () => terminalService.getDisplayTerminals().map((terminal) => ({
397784
+ id: terminal.id,
397785
+ title: terminal.title,
397786
+ type: terminal.type,
397787
+ cols: terminal.cols,
397788
+ rows: terminal.rows,
397789
+ runtimeState: terminal.runtimeState,
397790
+ lastExitCode: terminal.lastExitCode,
397791
+ monitorIdentity: terminalService.getMonitorIdentity(terminal.id) ?? void 0
397792
+ })),
397793
+ createTab: async (config2) => {
397794
+ const snapshot = terminalService.getDisplayTerminals();
397795
+ const normalized = createAutoTerminalConfig(snapshot, config2);
397796
+ const tab = await terminalService.createTerminal(normalized);
397797
+ return { id: tab.id };
397798
+ },
397799
+ write: async (terminalId, data) => {
397800
+ terminalService.write(terminalId, data);
397801
+ },
397802
+ writePaths: async (terminalId, paths) => {
397803
+ terminalService.writePaths(terminalId, paths);
397804
+ },
397805
+ resize: async (terminalId, cols, rows) => {
397806
+ terminalService.resize(terminalId, cols, rows);
397807
+ },
397808
+ kill: async (terminalId) => {
397809
+ if (terminalService.getDisplayTerminals().length <= 1) {
397810
+ throw new Error("Cannot close the last terminal tab.");
396673
397811
  }
396674
- const state = await observability.dashboard.state();
396675
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
396676
- res.end(JSON.stringify(state));
397812
+ terminalService.kill(terminalId);
397813
+ },
397814
+ reconnect: async (terminalId) => {
397815
+ const tab = await terminalService.reconnectTerminal(terminalId);
397816
+ return { id: tab.id };
397817
+ },
397818
+ setTitle: async (terminalId, title) => {
397819
+ terminalService.setTitle(terminalId, title);
397820
+ },
397821
+ setSelection: async (terminalId, selectionText) => {
397822
+ terminalService.setSelection(terminalId, selectionText);
397823
+ },
397824
+ getBufferDelta: async (terminalId, fromOffset) => {
397825
+ const data = terminalService.getBufferDelta(terminalId, fromOffset);
397826
+ const offset = terminalService.getCurrentOffset(terminalId);
397827
+ return { data, offset };
397828
+ },
397829
+ generateCommandDraft: async (terminalId, prompt, profileId) => {
397830
+ return await terminalCommandDraftService.generateCommandDraft({
397831
+ terminalId,
397832
+ prompt,
397833
+ profileId
397834
+ });
396677
397835
  }
396678
- }
396679
- ],
396680
- terminalBridge: {
396681
- listTerminals: () => terminalService.getDisplayTerminals().map((terminal) => ({
396682
- id: terminal.id,
396683
- title: terminal.title,
396684
- type: terminal.type,
396685
- cols: terminal.cols,
396686
- rows: terminal.rows,
396687
- runtimeState: terminal.runtimeState,
396688
- lastExitCode: terminal.lastExitCode,
396689
- monitorIdentity: terminalService.getMonitorIdentity(terminal.id) ?? void 0
396690
- })),
396691
- createTab: async (config2) => {
396692
- const snapshot = terminalService.getDisplayTerminals();
396693
- const normalized = createAutoTerminalConfig(snapshot, config2);
396694
- const tab = await terminalService.createTerminal(normalized);
396695
- return { id: tab.id };
396696
- },
396697
- write: async (terminalId, data) => {
396698
- terminalService.write(terminalId, data);
396699
397836
  },
396700
- writePaths: async (terminalId, paths) => {
396701
- terminalService.writePaths(terminalId, paths);
396702
- },
396703
- resize: async (terminalId, cols, rows) => {
396704
- terminalService.resize(terminalId, cols, rows);
396705
- },
396706
- kill: async (terminalId) => {
396707
- if (terminalService.getDisplayTerminals().length <= 1) {
396708
- throw new Error("Cannot close the last terminal tab.");
397837
+ filesystemBridge: {
397838
+ listDirectory: async (terminalId, dirPath) => {
397839
+ return await fileSystemService.listDirectory(terminalId, dirPath);
397840
+ },
397841
+ readTextFile: async (terminalId, filePath, options) => {
397842
+ return await fileSystemService.readTextFile(
397843
+ terminalId,
397844
+ filePath,
397845
+ options
397846
+ );
397847
+ },
397848
+ readFileBase64: async (terminalId, filePath, options) => {
397849
+ return await fileSystemService.readFileBase64(
397850
+ terminalId,
397851
+ filePath,
397852
+ options
397853
+ );
397854
+ },
397855
+ writeTextFile: async (terminalId, filePath, content) => {
397856
+ await fileSystemService.writeTextFile(
397857
+ terminalId,
397858
+ filePath,
397859
+ content
397860
+ );
397861
+ },
397862
+ writeFileBase64: async (terminalId, filePath, contentBase64, options) => {
397863
+ await fileSystemService.writeFileBase64(
397864
+ terminalId,
397865
+ filePath,
397866
+ contentBase64,
397867
+ options
397868
+ );
397869
+ },
397870
+ transferEntries: async (sourceTerminalId, sourcePaths, targetTerminalId, targetDirPath, options) => {
397871
+ return await fileSystemService.transferEntries(
397872
+ sourceTerminalId,
397873
+ sourcePaths,
397874
+ targetTerminalId,
397875
+ targetDirPath,
397876
+ options
397877
+ );
397878
+ },
397879
+ startTransfer: async (input) => {
397880
+ return fileTransferService.startTransfer(input);
397881
+ },
397882
+ getTransfer: async (transferId) => {
397883
+ return fileTransferService.getTransfer(transferId);
397884
+ },
397885
+ listTransfers: async (options) => {
397886
+ return fileTransferService.listTransfers(options);
397887
+ },
397888
+ cancelTransfer: async (transferId) => {
397889
+ return fileTransferService.cancelTransfer(transferId);
397890
+ },
397891
+ cancelTransferTask: async (transferId) => {
397892
+ return fileTransferService.cancelTransfer(transferId);
397893
+ },
397894
+ createDirectory: async (terminalId, dirPath) => {
397895
+ await fileSystemService.createDirectory(terminalId, dirPath);
397896
+ },
397897
+ createFile: async (terminalId, filePath) => {
397898
+ await fileSystemService.createFile(terminalId, filePath);
397899
+ },
397900
+ deletePath: async (terminalId, targetPath, options) => {
397901
+ await fileSystemService.deletePath(terminalId, targetPath, options);
397902
+ },
397903
+ renamePath: async (terminalId, sourcePath, targetPath) => {
397904
+ await fileSystemService.renamePath(
397905
+ terminalId,
397906
+ sourcePath,
397907
+ targetPath
397908
+ );
396709
397909
  }
396710
- terminalService.kill(terminalId);
396711
- },
396712
- reconnect: async (terminalId) => {
396713
- const tab = await terminalService.reconnectTerminal(terminalId);
396714
- return { id: tab.id };
396715
- },
396716
- setTitle: async (terminalId, title) => {
396717
- terminalService.setTitle(terminalId, title);
396718
- },
396719
- setSelection: async (terminalId, selectionText) => {
396720
- terminalService.setSelection(terminalId, selectionText);
396721
- },
396722
- getBufferDelta: async (terminalId, fromOffset) => {
396723
- const data = terminalService.getBufferDelta(terminalId, fromOffset);
396724
- const offset = terminalService.getCurrentOffset(terminalId);
396725
- return { data, offset };
396726
- },
396727
- generateCommandDraft: async (terminalId, prompt, profileId) => {
396728
- return await terminalCommandDraftService.generateCommandDraft({
396729
- terminalId,
396730
- prompt,
396731
- profileId
396732
- });
396733
- }
396734
- },
396735
- filesystemBridge: {
396736
- listDirectory: async (terminalId, dirPath) => {
396737
- return await fileSystemService.listDirectory(terminalId, dirPath);
396738
- },
396739
- readTextFile: async (terminalId, filePath, options) => {
396740
- return await fileSystemService.readTextFile(
396741
- terminalId,
396742
- filePath,
396743
- options
396744
- );
396745
- },
396746
- readFileBase64: async (terminalId, filePath, options) => {
396747
- return await fileSystemService.readFileBase64(
396748
- terminalId,
396749
- filePath,
396750
- options
396751
- );
396752
- },
396753
- writeTextFile: async (terminalId, filePath, content) => {
396754
- await fileSystemService.writeTextFile(
396755
- terminalId,
396756
- filePath,
396757
- content
396758
- );
396759
- },
396760
- writeFileBase64: async (terminalId, filePath, contentBase64, options) => {
396761
- await fileSystemService.writeFileBase64(
396762
- terminalId,
396763
- filePath,
396764
- contentBase64,
396765
- options
396766
- );
396767
- },
396768
- transferEntries: async (sourceTerminalId, sourcePaths, targetTerminalId, targetDirPath, options) => {
396769
- return await fileSystemService.transferEntries(
396770
- sourceTerminalId,
396771
- sourcePaths,
396772
- targetTerminalId,
396773
- targetDirPath,
396774
- options
396775
- );
396776
- },
396777
- startTransfer: async (input) => {
396778
- return fileTransferService.startTransfer(input);
396779
- },
396780
- getTransfer: async (transferId) => {
396781
- return fileTransferService.getTransfer(transferId);
396782
- },
396783
- listTransfers: async (options) => {
396784
- return fileTransferService.listTransfers(options);
396785
- },
396786
- cancelTransfer: async (transferId) => {
396787
- return fileTransferService.cancelTransfer(transferId);
396788
- },
396789
- cancelTransferTask: async (transferId) => {
396790
- return fileTransferService.cancelTransfer(transferId);
396791
- },
396792
- createDirectory: async (terminalId, dirPath) => {
396793
- await fileSystemService.createDirectory(terminalId, dirPath);
396794
397910
  },
396795
- createFile: async (terminalId, filePath) => {
396796
- await fileSystemService.createFile(terminalId, filePath);
396797
- },
396798
- deletePath: async (terminalId, targetPath, options) => {
396799
- await fileSystemService.deletePath(terminalId, targetPath, options);
396800
- },
396801
- renamePath: async (terminalId, sourcePath, targetPath) => {
396802
- await fileSystemService.renamePath(
396803
- terminalId,
396804
- sourcePath,
396805
- targetPath
396806
- );
396807
- }
396808
- },
396809
- profileBridge: {
396810
- getProfiles: () => {
396811
- const snapshot = settingsService.getSettings();
396812
- const modelNameById = new Map(
396813
- snapshot.models.items.map((item) => [item.id, item.model])
396814
- );
396815
- return {
396816
- activeProfileId: snapshot.models.activeProfileId,
396817
- profiles: snapshot.models.profiles.map((profile) => ({
396818
- id: profile.id,
396819
- name: profile.name,
396820
- globalModelId: profile.globalModelId,
396821
- modelName: modelNameById.get(profile.globalModelId)
396822
- }))
396823
- };
396824
- },
396825
- setActiveProfile: (profileId) => {
396826
- const snapshot = settingsService.getSettings();
396827
- const exists = snapshot.models.profiles.some(
396828
- (profile) => profile.id === profileId
396829
- );
396830
- if (!exists) {
396831
- throw new Error(`Profile not found: ${profileId}`);
396832
- }
396833
- settingsService.setSettings({
396834
- models: {
396835
- items: snapshot.models.items,
396836
- profiles: snapshot.models.profiles,
396837
- activeProfileId: profileId
397911
+ profileBridge: {
397912
+ getProfiles: () => {
397913
+ const snapshot = settingsService.getSettings();
397914
+ const modelNameById = new Map(
397915
+ snapshot.models.items.map((item) => [item.id, item.model])
397916
+ );
397917
+ return {
397918
+ activeProfileId: snapshot.models.activeProfileId,
397919
+ profiles: snapshot.models.profiles.map((profile) => ({
397920
+ id: profile.id,
397921
+ name: profile.name,
397922
+ globalModelId: profile.globalModelId,
397923
+ modelName: modelNameById.get(profile.globalModelId)
397924
+ }))
397925
+ };
397926
+ },
397927
+ setActiveProfile: (profileId) => {
397928
+ const snapshot = settingsService.getSettings();
397929
+ const exists = snapshot.models.profiles.some(
397930
+ (profile) => profile.id === profileId
397931
+ );
397932
+ if (!exists) {
397933
+ throw new Error(`Profile not found: ${profileId}`);
396838
397934
  }
396839
- });
396840
- const next = settingsService.getSettings();
396841
- agentService.updateSettings(next);
396842
- const modelNameById = new Map(
396843
- next.models.items.map((item) => [item.id, item.model])
396844
- );
396845
- return {
396846
- activeProfileId: next.models.activeProfileId,
396847
- profiles: next.models.profiles.map((profile) => ({
396848
- id: profile.id,
396849
- name: profile.name,
396850
- globalModelId: profile.globalModelId,
396851
- modelName: modelNameById.get(profile.globalModelId)
396852
- }))
396853
- };
396854
- },
396855
- probeModel: async (model) => {
396856
- return await modelCapabilityService.probe(model);
396857
- }
396858
- },
396859
- agentBridge: {
396860
- exportHistory: async (sessionId, mode) => {
396861
- await gatewayService.waitForRunCompletion(sessionId);
396862
- const backendSession = agentService.exportChatSession(sessionId);
396863
- if (!backendSession) {
396864
- throw new Error(`Session with ID ${sessionId} not found`);
396865
- }
396866
- const uiSession = uiHistoryService.getSession(sessionId);
396867
- if (mode === "simple") {
396868
- const markdown = uiHistoryService.toReadableMarkdown(
396869
- uiSession?.messages || [],
396870
- uiSession?.title || backendSession.title
397935
+ settingsService.setSettings({
397936
+ models: {
397937
+ items: snapshot.models.items,
397938
+ profiles: snapshot.models.profiles,
397939
+ activeProfileId: profileId
397940
+ }
397941
+ });
397942
+ const next = settingsService.getSettings();
397943
+ agentService.updateSettings(next);
397944
+ const modelNameById = new Map(
397945
+ next.models.items.map((item) => [item.id, item.model])
396871
397946
  );
396872
397947
  return {
396873
- sessionId,
396874
- mode,
396875
- title: uiSession?.title || backendSession.title,
396876
- content: markdown
397948
+ activeProfileId: next.models.activeProfileId,
397949
+ profiles: next.models.profiles.map((profile) => ({
397950
+ id: profile.id,
397951
+ name: profile.name,
397952
+ globalModelId: profile.globalModelId,
397953
+ modelName: modelNameById.get(profile.globalModelId)
397954
+ }))
396877
397955
  };
397956
+ },
397957
+ probeModel: async (model) => {
397958
+ return await modelCapabilityService.probe(model);
396878
397959
  }
396879
- return {
396880
- sessionId: backendSession.id,
396881
- mode,
396882
- title: uiSession?.title || backendSession.title,
396883
- lastCheckpointOffset: backendSession.lastCheckpointOffset,
396884
- createdAt: new Date(backendSession.createdAt).toISOString(),
396885
- updatedAt: new Date(backendSession.updatedAt).toISOString(),
396886
- frontendMessages: uiSession?.messages || [],
396887
- backendMessages: backendSession.messages.map((msg) => ({
396888
- messageId: msg.id,
396889
- messageType: msg.type,
396890
- messageData: msg.data
396891
- }))
396892
- };
396893
- },
396894
- getAllChatHistory: () => agentService.getAllChatHistory(),
396895
- loadChatSession: (sessionId) => agentService.loadChatSession(sessionId),
396896
- getUiMessages: (sessionId) => uiHistoryService.getMessages(sessionId)
396897
- },
396898
- systemBridge: {
396899
- saveImageAttachment: async (payload) => {
396900
- return await imageAttachmentService.saveImageAttachment(payload);
396901
- }
396902
- },
396903
- skillBridge: {
396904
- reload: async () => {
396905
- return await skillService.reload();
396906
- },
396907
- getAll: async () => {
396908
- return await skillService.getAll();
396909
- },
396910
- getEnabled: async () => {
396911
- return await skillService.getEnabledSkills();
396912
397960
  },
396913
- create: async () => {
396914
- return await skillService.createSkillFromTemplate();
396915
- },
396916
- delete: async (fileName) => {
396917
- await skillService.deleteSkillFile(fileName);
396918
- return await skillService.getAll();
396919
- },
396920
- listSkills: async () => {
396921
- const snapshot = settingsService.getSettings();
396922
- const enabledMap = snapshot.tools?.skills ?? {};
396923
- const skills = await skillService.getAll();
396924
- return skills.map((skill) => ({
396925
- name: skill.name,
396926
- description: skill.description,
396927
- enabled: enabledMap[skill.name] !== false
396928
- }));
396929
- },
396930
- setSkillEnabled: async (name, enabled) => {
396931
- const snapshot = settingsService.getSettings();
396932
- const nextSkills = { ...snapshot.tools?.skills ?? {} };
396933
- nextSkills[name] = enabled;
396934
- settingsService.setSettings({
396935
- tools: {
396936
- builtIn: snapshot.tools?.builtIn ?? {},
396937
- skills: nextSkills
397961
+ agentBridge: {
397962
+ exportHistory: async (sessionId, mode) => {
397963
+ await gatewayService.waitForRunCompletion(sessionId);
397964
+ const backendSession = agentService.exportChatSession(sessionId);
397965
+ if (!backendSession) {
397966
+ throw new Error(`Session with ID ${sessionId} not found`);
397967
+ }
397968
+ const uiSession = uiHistoryService.getSession(sessionId);
397969
+ if (mode === "simple") {
397970
+ const markdown = uiHistoryService.toReadableMarkdown(
397971
+ uiSession?.messages || [],
397972
+ uiSession?.title || backendSession.title
397973
+ );
397974
+ return {
397975
+ sessionId,
397976
+ mode,
397977
+ title: uiSession?.title || backendSession.title,
397978
+ content: markdown
397979
+ };
396938
397980
  }
396939
- });
396940
- const next = settingsService.getSettings();
396941
- agentService.updateSettings(next);
396942
- const skills = await skillService.getAll();
396943
- const summary = buildSkillStatusSummary(skills, next.tools?.skills);
396944
- gatewayService.broadcastRaw("skills:updated", summary);
396945
- return summary;
396946
- }
396947
- },
396948
- memoryBridge: {
396949
- get: async () => {
396950
- return await memoryService.getMemorySnapshot(
396951
- settingsService.getSettings().agentSettings?.activeProfileId || null
396952
- );
396953
- },
396954
- setContent: async (content) => {
396955
- const snapshot = await memoryService.writeMemory(
396956
- content,
396957
- settingsService.getSettings().agentSettings?.activeProfileId || null
396958
- );
396959
- gatewayService.broadcastRaw("memory:updated", snapshot);
396960
- return snapshot;
396961
- },
396962
- search: async (query, limit2) => {
396963
- const { content } = await memoryService.getMemorySnapshot(
396964
- settingsService.getSettings().agentSettings?.activeProfileId || null
396965
- );
396966
- return searchMemory(content, query, limit2 ?? 10);
396967
- },
396968
- append: async (note) => {
396969
- const profileId = settingsService.getSettings().agentSettings?.activeProfileId || null;
396970
- const { content } = await memoryService.getMemorySnapshot(profileId);
396971
- const next = appendMemoryNote(content, note);
396972
- const snapshot = await memoryService.writeMemory(next, profileId);
396973
- gatewayService.broadcastRaw("memory:updated", snapshot);
396974
- return snapshot;
396975
- }
396976
- },
396977
- agentSettingsBridge: {
396978
- get: () => agentSettingProfileService.getState(),
396979
- saveCurrent: async () => {
396980
- const result = await agentSettingProfileService.saveCurrent();
396981
- broadcastAgentSettingResult(result);
396982
- return result;
397981
+ return {
397982
+ sessionId: backendSession.id,
397983
+ mode,
397984
+ title: uiSession?.title || backendSession.title,
397985
+ lastCheckpointOffset: backendSession.lastCheckpointOffset,
397986
+ createdAt: new Date(backendSession.createdAt).toISOString(),
397987
+ updatedAt: new Date(backendSession.updatedAt).toISOString(),
397988
+ frontendMessages: uiSession?.messages || [],
397989
+ backendMessages: backendSession.messages.map((msg) => ({
397990
+ messageId: msg.id,
397991
+ messageType: msg.type,
397992
+ messageData: msg.data
397993
+ }))
397994
+ };
397995
+ },
397996
+ getAllChatHistory: () => agentService.getAllChatHistory(),
397997
+ loadChatSession: (sessionId) => agentService.loadChatSession(sessionId),
397998
+ getUiMessages: (sessionId) => uiHistoryService.getMessages(sessionId)
396983
397999
  },
396984
- apply: async (profileId) => {
396985
- const result = await agentSettingProfileService.apply(profileId);
396986
- broadcastAgentSettingResult(result);
396987
- return result;
398000
+ systemBridge: {
398001
+ saveImageAttachment: async (payload) => {
398002
+ return await imageAttachmentService.saveImageAttachment(payload);
398003
+ }
396988
398004
  },
396989
- overwrite: async (profileId) => {
396990
- const result = await agentSettingProfileService.overwrite(profileId);
396991
- broadcastAgentSettingResult(result);
396992
- return result;
398005
+ skillBridge: {
398006
+ reload: async () => {
398007
+ return await skillService.reload();
398008
+ },
398009
+ getAll: async () => {
398010
+ return await skillService.getAll();
398011
+ },
398012
+ getEnabled: async () => {
398013
+ return await skillService.getEnabledSkills();
398014
+ },
398015
+ create: async () => {
398016
+ return await skillService.createSkillFromTemplate();
398017
+ },
398018
+ delete: async (fileName) => {
398019
+ await skillService.deleteSkillFile(fileName);
398020
+ return await skillService.getAll();
398021
+ },
398022
+ listSkills: async () => {
398023
+ const snapshot = settingsService.getSettings();
398024
+ const enabledMap = snapshot.tools?.skills ?? {};
398025
+ const skills = await skillService.getAll();
398026
+ return skills.map((skill) => ({
398027
+ name: skill.name,
398028
+ description: skill.description,
398029
+ enabled: enabledMap[skill.name] !== false
398030
+ }));
398031
+ },
398032
+ setSkillEnabled: async (name, enabled) => {
398033
+ const snapshot = settingsService.getSettings();
398034
+ const nextSkills = { ...snapshot.tools?.skills ?? {} };
398035
+ nextSkills[name] = enabled;
398036
+ settingsService.setSettings({
398037
+ tools: {
398038
+ builtIn: snapshot.tools?.builtIn ?? {},
398039
+ skills: nextSkills
398040
+ }
398041
+ });
398042
+ const next = settingsService.getSettings();
398043
+ agentService.updateSettings(next);
398044
+ const skills = await skillService.getAll();
398045
+ const summary = buildSkillStatusSummary(skills, next.tools?.skills);
398046
+ gatewayService.broadcastRaw("skills:updated", summary);
398047
+ return summary;
398048
+ }
396993
398049
  },
396994
- delete: async (profileId) => {
396995
- const result = await agentSettingProfileService.delete(profileId);
396996
- broadcastAgentSettingResult(result);
396997
- return result;
396998
- }
396999
- },
397000
- settingsBridge: {
397001
- getSettings: () => settingsService.getSettings(),
397002
- setSettings: async (patch) => {
397003
- if (patch?.gateway?.ws) {
397004
- throw new Error(
397005
- "settings.gateway.ws is not configurable via websocket RPC."
398050
+ memoryBridge: {
398051
+ get: async () => {
398052
+ return await memoryService.getMemorySnapshot(
398053
+ settingsService.getSettings().agentSettings?.activeProfileId || null
397006
398054
  );
398055
+ },
398056
+ setContent: async (content) => {
398057
+ const snapshot = await memoryService.writeMemory(
398058
+ content,
398059
+ settingsService.getSettings().agentSettings?.activeProfileId || null
398060
+ );
398061
+ gatewayService.broadcastRaw("memory:updated", snapshot);
398062
+ return snapshot;
398063
+ },
398064
+ search: async (query, limit2) => {
398065
+ const { content } = await memoryService.getMemorySnapshot(
398066
+ settingsService.getSettings().agentSettings?.activeProfileId || null
398067
+ );
398068
+ return searchMemory(content, query, limit2 ?? 10);
398069
+ },
398070
+ append: async (note) => {
398071
+ const profileId = settingsService.getSettings().agentSettings?.activeProfileId || null;
398072
+ const { content } = await memoryService.getMemorySnapshot(profileId);
398073
+ const next = appendMemoryNote(content, note);
398074
+ const snapshot = await memoryService.writeMemory(next, profileId);
398075
+ gatewayService.broadcastRaw("memory:updated", snapshot);
398076
+ return snapshot;
397007
398077
  }
397008
- settingsService.setSettings(patch);
397009
- const next = settingsService.getSettings();
397010
- agentService.updateSettings(next);
397011
- return next;
397012
- }
397013
- },
397014
- commandPolicyBridge: {
397015
- getLists: async () => {
397016
- return await commandPolicyService.getLists();
397017
398078
  },
397018
- addRule: async (listName, rule) => {
397019
- return await commandPolicyService.addRule(listName, rule);
398079
+ agentSettingsBridge: {
398080
+ get: () => agentSettingProfileService.getState(),
398081
+ saveCurrent: async () => {
398082
+ const result = await agentSettingProfileService.saveCurrent();
398083
+ broadcastAgentSettingResult(result);
398084
+ return result;
398085
+ },
398086
+ apply: async (profileId) => {
398087
+ const result = await agentSettingProfileService.apply(profileId);
398088
+ broadcastAgentSettingResult(result);
398089
+ return result;
398090
+ },
398091
+ overwrite: async (profileId) => {
398092
+ const result = await agentSettingProfileService.overwrite(profileId);
398093
+ broadcastAgentSettingResult(result);
398094
+ return result;
398095
+ },
398096
+ delete: async (profileId) => {
398097
+ const result = await agentSettingProfileService.delete(profileId);
398098
+ broadcastAgentSettingResult(result);
398099
+ return result;
398100
+ }
397020
398101
  },
397021
- deleteRule: async (listName, rule) => {
397022
- return await commandPolicyService.deleteRule(listName, rule);
397023
- }
397024
- },
397025
- toolsBridge: {
397026
- reloadMcp: async () => {
397027
- return await mcpToolService.reloadAll();
398102
+ settingsBridge: {
398103
+ getSettings: () => settingsService.getSettings(),
398104
+ setSettings: async (patch) => {
398105
+ if (patch?.gateway?.ws) {
398106
+ throw new Error(
398107
+ "settings.gateway.ws is not configurable via websocket RPC."
398108
+ );
398109
+ }
398110
+ settingsService.setSettings(patch);
398111
+ const next = settingsService.getSettings();
398112
+ agentService.updateSettings(next);
398113
+ return next;
398114
+ },
398115
+ // v3.2.18: settings backup/restore/export/import.
398116
+ listBackups: () => settingsBackupService.list(),
398117
+ restoreBackup: async (name) => settingsBackupService.restore(name),
398118
+ export: () => settingsBackupService.export(),
398119
+ import: async (content) => settingsBackupService.import(content)
397028
398120
  },
397029
- getMcp: () => mcpToolService.getSummaries(),
397030
- setMcpEnabled: async (name, enabled) => {
397031
- return await mcpToolService.setServerEnabled(name, enabled);
398121
+ // v3.2.18: cross-session history search.
398122
+ historyBridge: {
398123
+ getAllSessions: async () => {
398124
+ try {
398125
+ const all = historyStore.listChatSessions();
398126
+ if (Array.isArray(all) && all.length > 0) {
398127
+ return all;
398128
+ }
398129
+ } catch (e) {
398130
+ console.warn(
398131
+ "[history:search] store load failed, falling back to metadata:",
398132
+ e instanceof Error ? e.message : e
398133
+ );
398134
+ }
398135
+ return agentService.getAllChatHistory() ?? [];
398136
+ }
397032
398137
  },
397033
- getBuiltIn: () => {
397034
- const settings = settingsService.getSettings();
397035
- return buildBuiltInToolStatusSummary(settings.tools?.builtIn);
398138
+ commandPolicyBridge: {
398139
+ getLists: async () => {
398140
+ return await commandPolicyService.getLists();
398141
+ },
398142
+ addRule: async (listName, rule) => {
398143
+ return await commandPolicyService.addRule(listName, rule);
398144
+ },
398145
+ deleteRule: async (listName, rule) => {
398146
+ return await commandPolicyService.deleteRule(listName, rule);
398147
+ }
397036
398148
  },
397037
- setBuiltInEnabled: async (name, enabled) => {
397038
- const settings = settingsService.getSettings();
397039
- const nextBuiltIn = { ...settings.tools?.builtIn ?? {} };
397040
- nextBuiltIn[name] = enabled;
397041
- settingsService.setSettings({
397042
- tools: {
397043
- builtIn: nextBuiltIn,
397044
- skills: settings.tools?.skills ?? {}
397045
- }
397046
- });
397047
- const next = settingsService.getSettings();
397048
- agentService.updateSettings(next);
397049
- const summary = buildBuiltInToolStatusSummary(next.tools?.builtIn);
397050
- gatewayService.broadcastRaw("tools:builtInUpdated", summary);
397051
- return summary;
397052
- }
397053
- },
397054
- // Observability bridge (v2.9.x): exposes the 9 platform capabilities
397055
- // (metrics export, secrets, on-call, cost, recording, gitops, playbooks,
397056
- // cloud, live dashboard) as observability:* RPC methods.
397057
- observabilityBridge: createObservabilityBridge({
397058
- observability: () => observability,
397059
- terminalService: () => terminalService
397060
- })
397061
- })
398149
+ toolsBridge: {
398150
+ reloadMcp: async () => {
398151
+ return await mcpToolService.reloadAll();
398152
+ },
398153
+ getMcp: () => mcpToolService.getSummaries(),
398154
+ setMcpEnabled: async (name, enabled) => {
398155
+ return await mcpToolService.setServerEnabled(name, enabled);
398156
+ },
398157
+ getBuiltIn: () => {
398158
+ const settings = settingsService.getSettings();
398159
+ return buildBuiltInToolStatusSummary(settings.tools?.builtIn);
398160
+ },
398161
+ setBuiltInEnabled: async (name, enabled) => {
398162
+ const settings = settingsService.getSettings();
398163
+ const nextBuiltIn = { ...settings.tools?.builtIn ?? {} };
398164
+ nextBuiltIn[name] = enabled;
398165
+ settingsService.setSettings({
398166
+ tools: {
398167
+ builtIn: nextBuiltIn,
398168
+ skills: settings.tools?.skills ?? {}
398169
+ }
398170
+ });
398171
+ const next = settingsService.getSettings();
398172
+ agentService.updateSettings(next);
398173
+ const summary = buildBuiltInToolStatusSummary(next.tools?.builtIn);
398174
+ gatewayService.broadcastRaw("tools:builtInUpdated", summary);
398175
+ return summary;
398176
+ }
398177
+ },
398178
+ // Observability bridge (v2.9.x): exposes the 9 platform capabilities
398179
+ // (metrics export, secrets, on-call, cost, recording, gitops, playbooks,
398180
+ // cloud, live dashboard) as observability:* RPC methods.
398181
+ observabilityBridge: createObservabilityBridge({
398182
+ observability: () => observability,
398183
+ terminalService: () => terminalService
398184
+ })
398185
+ });
398186
+ restDispatchTarget = adapter;
398187
+ return adapter;
398188
+ }
397062
398189
  });
397063
398190
  await wsGatewayControlService.applyPolicy(startupPolicy);
397064
398191
  const shutdown = async (signal) => {