neuralos 3.2.18 → 3.3.0

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 +1765 -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,90 @@ 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
+ if (chain.length === 0) {
258130
+ return {
258131
+ error: new Error("model failover chain is empty \u2014 no candidates to try"),
258132
+ attempts
258133
+ };
258134
+ }
258135
+ for (const candidate of chain) {
258136
+ const started = Date.now();
258137
+ try {
258138
+ const value = await operation(candidate);
258139
+ const attempt = { model: candidate.model, ok: true, durationMs: Date.now() - started };
258140
+ attempts.push(attempt);
258141
+ opts.onAttempt?.(attempt);
258142
+ return { value, attempts, usedModel: candidate.model };
258143
+ } catch (error40) {
258144
+ const decision = isFailoverEligible(error40);
258145
+ const attempt = {
258146
+ model: candidate.model,
258147
+ ok: false,
258148
+ reason: decision.reason,
258149
+ durationMs: Date.now() - started
258150
+ };
258151
+ attempts.push(attempt);
258152
+ opts.onAttempt?.(attempt);
258153
+ if (!decision.eligible) {
258154
+ return { error: error40, attempts };
258155
+ }
258156
+ lastError = error40;
258157
+ }
258158
+ }
258159
+ return { error: lastError, attempts };
258160
+ }
258161
+ var FAILOVER_ERROR_PATTERNS, NON_FAILOVER_PATTERNS;
258162
+ var init_modelFailover = __esm({
258163
+ "../../packages/backend/src/services/AgentHelper/utils/modelFailover.ts"() {
258164
+ "use strict";
258165
+ FAILOVER_ERROR_PATTERNS = [
258166
+ { re: /\b429\b|rate.?limit|too many requests|quota/i, reason: "rate-limited" },
258167
+ { re: /\b5\d\d\b|internal server error|bad gateway|service unavailable|upstream/i, reason: "provider-5xx" },
258168
+ { re: /network|fetch failed|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up/i, reason: "network" },
258169
+ { re: /\b401\b|unauthorized|invalid api key|authentication/i, reason: "auth" },
258170
+ { re: /overloaded|capacity|model .* not found|no such model/i, reason: "model-unavailable" }
258171
+ ];
258172
+ NON_FAILOVER_PATTERNS = [
258173
+ /context.?length|token limit|maximum context/i,
258174
+ /invalid request|malformed|bad request|\b400\b/i,
258175
+ /aborted|AbortError/i
258176
+ // user cancelled — never retry
258177
+ ];
258178
+ }
258179
+ });
258180
+
257993
258181
  // ../../packages/backend/src/services/history/HistorySqliteStore.ts
257994
258182
  var HistorySqliteStore_exports = {};
257995
258183
  __export(HistorySqliteStore_exports, {
@@ -261975,6 +262163,136 @@ var require_websocket_server = __commonJS({
261975
262163
  }
261976
262164
  });
261977
262165
 
262166
+ // ../../packages/backend/src/services/history/historySearch.ts
262167
+ var historySearch_exports = {};
262168
+ __export(historySearch_exports, {
262169
+ buildSnippet: () => buildSnippet,
262170
+ extractMessageText: () => extractMessageText2,
262171
+ findMatches: () => findMatches,
262172
+ normalizeMessages: () => normalizeMessages,
262173
+ searchChatHistory: () => searchChatHistory
262174
+ });
262175
+ function extractMessageText2(data) {
262176
+ if (!data || typeof data !== "object") return "";
262177
+ let obj = data;
262178
+ for (let i = 0; i < 3 && obj.data && typeof obj.data === "object" && !Array.isArray(obj.data); i++) {
262179
+ const inner = obj.data;
262180
+ if (inner.content !== void 0 || inner.data !== void 0) {
262181
+ obj = inner;
262182
+ } else {
262183
+ break;
262184
+ }
262185
+ }
262186
+ const content = obj.content;
262187
+ if (typeof content === "string") return content;
262188
+ if (Array.isArray(content)) {
262189
+ return content.map((part) => {
262190
+ if (typeof part === "string") return part;
262191
+ if (part && typeof part === "object") {
262192
+ const p = part;
262193
+ if (typeof p.text === "string") return p.text;
262194
+ }
262195
+ return "";
262196
+ }).join(" ");
262197
+ }
262198
+ return "";
262199
+ }
262200
+ function buildSnippet(text, offset, length) {
262201
+ const start = Math.max(0, offset - SNIPPET_CONTEXT);
262202
+ const end = Math.min(text.length, offset + length + SNIPPET_CONTEXT);
262203
+ const prefix = start > 0 ? "\u2026" : "";
262204
+ const suffix = end < text.length ? "\u2026" : "";
262205
+ return prefix + text.slice(start, end).replace(/\s+/g, " ").trim() + suffix;
262206
+ }
262207
+ function findMatches(text, query, wholeWord) {
262208
+ if (!text || !query) return [];
262209
+ const haystack = text.toLowerCase();
262210
+ const needle = query.toLowerCase();
262211
+ const out = [];
262212
+ if (wholeWord) {
262213
+ const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
262214
+ let m2;
262215
+ while ((m2 = re.exec(haystack)) !== null) {
262216
+ out.push({ offset: m2.index, length: m2[0].length });
262217
+ if (m2.index === re.lastIndex) re.lastIndex++;
262218
+ }
262219
+ } else {
262220
+ let idx = haystack.indexOf(needle);
262221
+ while (idx !== -1) {
262222
+ out.push({ offset: idx, length: needle.length });
262223
+ idx = haystack.indexOf(needle, idx + needle.length);
262224
+ }
262225
+ }
262226
+ return out;
262227
+ }
262228
+ function normalizeMessages(messages) {
262229
+ if (!messages) return [];
262230
+ if (Array.isArray(messages)) return messages;
262231
+ if (messages instanceof Map) return [...messages.values()];
262232
+ if (typeof messages === "object") return Object.values(messages);
262233
+ return [];
262234
+ }
262235
+ function searchChatHistory(sessions, query, options = {}) {
262236
+ const sessionLimit = options.sessionLimit ?? DEFAULT_SESSION_LIMIT;
262237
+ const snippetLimit = options.snippetLimit ?? DEFAULT_SNIPPET_LIMIT;
262238
+ const wholeWord = options.wholeWord ?? false;
262239
+ const includeTitles = options.includeTitles ?? true;
262240
+ const trimmed = (query ?? "").trim();
262241
+ if (!trimmed) {
262242
+ return { query: trimmed, totalSessions: 0, totalMatches: 0, sessions: [], truncated: false };
262243
+ }
262244
+ const results = [];
262245
+ let totalMatches = 0;
262246
+ for (const session of sessions) {
262247
+ let matchCount = 0;
262248
+ const matches = [];
262249
+ const titleHit = includeTitles && session.title ? findMatches(session.title, trimmed, wholeWord) : [];
262250
+ for (const message of normalizeMessages(session.messages)) {
262251
+ const text = extractMessageText2(message?.data);
262252
+ if (!text) continue;
262253
+ const hits = findMatches(text, trimmed, wholeWord);
262254
+ for (const hit of hits) {
262255
+ matchCount++;
262256
+ totalMatches++;
262257
+ if (matches.length < snippetLimit) {
262258
+ matches.push({
262259
+ messageId: String(message?.id ?? ""),
262260
+ type: String(message?.type ?? ""),
262261
+ snippet: buildSnippet(text, hit.offset, hit.length),
262262
+ offset: hit.offset
262263
+ });
262264
+ }
262265
+ }
262266
+ }
262267
+ if (matchCount === 0 && titleHit.length === 0) continue;
262268
+ results.push({
262269
+ sessionId: session.id,
262270
+ sessionTitle: session.title,
262271
+ updatedAt: session.updatedAt,
262272
+ matchCount: matchCount + titleHit.length,
262273
+ matches
262274
+ });
262275
+ }
262276
+ results.sort((a, b) => b.matchCount - a.matchCount || b.updatedAt - a.updatedAt);
262277
+ const truncated = results.length > sessionLimit;
262278
+ return {
262279
+ query: trimmed,
262280
+ totalSessions: results.length,
262281
+ totalMatches,
262282
+ sessions: results.slice(0, sessionLimit),
262283
+ truncated
262284
+ };
262285
+ }
262286
+ var SNIPPET_CONTEXT, DEFAULT_SESSION_LIMIT, DEFAULT_SNIPPET_LIMIT;
262287
+ var init_historySearch = __esm({
262288
+ "../../packages/backend/src/services/history/historySearch.ts"() {
262289
+ "use strict";
262290
+ SNIPPET_CONTEXT = 60;
262291
+ DEFAULT_SESSION_LIMIT = 20;
262292
+ DEFAULT_SNIPPET_LIMIT = 3;
262293
+ }
262294
+ });
262295
+
261978
262296
  // ../../node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js
261979
262297
  var require_code = __commonJS({
261980
262298
  "../../node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js"(exports2) {
@@ -274408,12 +274726,12 @@ var require_dist2 = __commonJS({
274408
274726
  throw new Error(`Unknown format "${name}"`);
274409
274727
  return f;
274410
274728
  };
274411
- function addFormats(ajv, list, fs22, exportName) {
274729
+ function addFormats(ajv, list, fs23, exportName) {
274412
274730
  var _a3;
274413
274731
  var _b;
274414
274732
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
274415
274733
  for (const f of list)
274416
- ajv.addFormat(f, fs22[f]);
274734
+ ajv.addFormat(f, fs23[f]);
274417
274735
  }
274418
274736
  module2.exports = exports2 = formatsPlugin;
274419
274737
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -274426,7 +274744,7 @@ var require_windows = __commonJS({
274426
274744
  "../../node_modules/isexe/windows.js"(exports2, module2) {
274427
274745
  module2.exports = isexe;
274428
274746
  isexe.sync = sync;
274429
- var fs22 = require("fs");
274747
+ var fs23 = require("fs");
274430
274748
  function checkPathExt(path32, options) {
274431
274749
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
274432
274750
  if (!pathext) {
@@ -274451,12 +274769,12 @@ var require_windows = __commonJS({
274451
274769
  return checkPathExt(path32, options);
274452
274770
  }
274453
274771
  function isexe(path32, options, cb) {
274454
- fs22.stat(path32, function(er, stat) {
274772
+ fs23.stat(path32, function(er, stat) {
274455
274773
  cb(er, er ? false : checkStat(stat, path32, options));
274456
274774
  });
274457
274775
  }
274458
274776
  function sync(path32, options) {
274459
- return checkStat(fs22.statSync(path32), path32, options);
274777
+ return checkStat(fs23.statSync(path32), path32, options);
274460
274778
  }
274461
274779
  }
274462
274780
  });
@@ -274466,14 +274784,14 @@ var require_mode = __commonJS({
274466
274784
  "../../node_modules/isexe/mode.js"(exports2, module2) {
274467
274785
  module2.exports = isexe;
274468
274786
  isexe.sync = sync;
274469
- var fs22 = require("fs");
274787
+ var fs23 = require("fs");
274470
274788
  function isexe(path32, options, cb) {
274471
- fs22.stat(path32, function(er, stat) {
274789
+ fs23.stat(path32, function(er, stat) {
274472
274790
  cb(er, er ? false : checkStat(stat, options));
274473
274791
  });
274474
274792
  }
274475
274793
  function sync(path32, options) {
274476
- return checkStat(fs22.statSync(path32), options);
274794
+ return checkStat(fs23.statSync(path32), options);
274477
274795
  }
274478
274796
  function checkStat(stat, options) {
274479
274797
  return stat.isFile() && checkMode(stat, options);
@@ -274497,7 +274815,7 @@ var require_mode = __commonJS({
274497
274815
  // ../../node_modules/isexe/index.js
274498
274816
  var require_isexe = __commonJS({
274499
274817
  "../../node_modules/isexe/index.js"(exports2, module2) {
274500
- var fs22 = require("fs");
274818
+ var fs23 = require("fs");
274501
274819
  var core2;
274502
274820
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
274503
274821
  core2 = require_windows();
@@ -274761,16 +275079,16 @@ var require_shebang_command = __commonJS({
274761
275079
  var require_readShebang = __commonJS({
274762
275080
  "../../node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
274763
275081
  "use strict";
274764
- var fs22 = require("fs");
275082
+ var fs23 = require("fs");
274765
275083
  var shebangCommand = require_shebang_command();
274766
275084
  function readShebang(command) {
274767
275085
  const size = 150;
274768
275086
  const buffer = Buffer.alloc(size);
274769
275087
  let fd;
274770
275088
  try {
274771
- fd = fs22.openSync(command, "r");
274772
- fs22.readSync(fd, buffer, 0, size, 0);
274773
- fs22.closeSync(fd);
275089
+ fd = fs23.openSync(command, "r");
275090
+ fs23.readSync(fd, buffer, 0, size, 0);
275091
+ fs23.closeSync(fd);
274774
275092
  } catch (e) {
274775
275093
  }
274776
275094
  return shebangCommand(buffer.toString());
@@ -275398,7 +275716,7 @@ var require_has_flag = __commonJS({
275398
275716
  var require_supports_color = __commonJS({
275399
275717
  "../../node_modules/supports-color/index.js"(exports2, module2) {
275400
275718
  "use strict";
275401
- var os7 = require("os");
275719
+ var os8 = require("os");
275402
275720
  var tty = require("tty");
275403
275721
  var hasFlag = require_has_flag();
275404
275722
  var { env } = process;
@@ -275446,7 +275764,7 @@ var require_supports_color = __commonJS({
275446
275764
  return min;
275447
275765
  }
275448
275766
  if (process.platform === "win32") {
275449
- const osRelease = os7.release().split(".");
275767
+ const osRelease = os8.release().split(".");
275450
275768
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
275451
275769
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
275452
275770
  }
@@ -286297,11 +286615,11 @@ var require_jsmconsumer_api = __commonJS({
286297
286615
  consumerName = cfg.name ?? cfg.durable_name ?? "";
286298
286616
  }
286299
286617
  if (consumerName !== "") {
286300
- let fs22 = cfg.filter_subject ?? void 0;
286301
- if (fs22 === ">") {
286302
- fs22 = void 0;
286618
+ let fs23 = cfg.filter_subject ?? void 0;
286619
+ if (fs23 === ">") {
286620
+ fs23 = void 0;
286303
286621
  }
286304
- subj = fs22 !== void 0 ? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs22}` : `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
286622
+ subj = fs23 !== void 0 ? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs23}` : `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
286305
286623
  } else {
286306
286624
  subj = cfg.durable_name ? `${this.prefix}.CONSUMER.DURABLE.CREATE.${stream}.${cfg.durable_name}` : `${this.prefix}.CONSUMER.CREATE.${stream}`;
286307
286625
  }
@@ -290852,6 +291170,7 @@ var init_natsEventBus = __esm({
290852
291170
 
290853
291171
  // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
290854
291172
  var import_node_path28 = __toESM(require("node:path"), 1);
291173
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
290855
291174
  var import_node_process2 = __toESM(require("node:process"), 1);
290856
291175
 
290857
291176
  // ../../packages/backend/src/services/TerminalService.ts
@@ -293693,19 +294012,19 @@ var SSHBackend = class _SSHBackend {
293693
294012
  ),
293694
294013
  this.execCollect(client2, "hostname", 8e3)
293695
294014
  ]);
293696
- let os7 = "unix";
294015
+ let os8 = "unix";
293697
294016
  const releaseMatch = osRelease.stdout.match(/^ID=(.*)$/m);
293698
294017
  if (releaseMatch) {
293699
- os7 = releaseMatch[1].replace(/"/g, "");
294018
+ os8 = releaseMatch[1].replace(/"/g, "");
293700
294019
  } else {
293701
294020
  const unameS2 = uname.stdout.split(" ")[0].toLowerCase();
293702
- os7 = unameS2 || "unix";
294021
+ os8 = unameS2 || "unix";
293703
294022
  }
293704
294023
  const unameS = uname.stdout.split(" ")[0].toLowerCase();
293705
294024
  const platform2 = unameS.includes("darwin") ? "darwin" : unameS.includes("linux") ? "linux" : "unix";
293706
294025
  const parts = uname.stdout.split(" ");
293707
294026
  const next = {
293708
- os: os7,
294027
+ os: os8,
293709
294028
  platform: platform2,
293710
294029
  release: parts[2] || "",
293711
294030
  arch: parts[parts.length - 2] || "",
@@ -300516,6 +300835,9 @@ var TerminalService = class _TerminalService {
300516
300835
  terminalIdsBeingKilled = /* @__PURE__ */ new Set();
300517
300836
  autoReconnect = new AutoReconnect({ maxAttempts: 10 });
300518
300837
  terminalClosedListeners = /* @__PURE__ */ new Set();
300838
+ /** v3.2.18: idle-timeout tracking hooks. */
300839
+ terminalCreatedListeners = /* @__PURE__ */ new Set();
300840
+ terminalDataListeners = /* @__PURE__ */ new Set();
300519
300841
  /** Optional session logger (records terminal output to disk per session). */
300520
300842
  sessionLogger = null;
300521
300843
  sessionLogStarted = /* @__PURE__ */ new Set();
@@ -300565,6 +300887,38 @@ var TerminalService = class _TerminalService {
300565
300887
  this.terminalClosedListeners.delete(listener);
300566
300888
  };
300567
300889
  }
300890
+ /** v3.2.18: fires when a terminal tab is created (idle-timeout tracking). */
300891
+ onTerminalCreated(listener) {
300892
+ this.terminalCreatedListeners.add(listener);
300893
+ return () => {
300894
+ this.terminalCreatedListeners.delete(listener);
300895
+ };
300896
+ }
300897
+ /** v3.2.18: fires on any terminal data event (idle-timeout activity). */
300898
+ onTerminalData(listener) {
300899
+ this.terminalDataListeners.add(listener);
300900
+ return () => {
300901
+ this.terminalDataListeners.delete(listener);
300902
+ };
300903
+ }
300904
+ /** v3.2.18: notify created-listeners (called from createTerminal paths). */
300905
+ notifyTerminalCreated(terminalId) {
300906
+ for (const listener of this.terminalCreatedListeners) {
300907
+ try {
300908
+ listener(terminalId);
300909
+ } catch {
300910
+ }
300911
+ }
300912
+ }
300913
+ /** v3.2.18: notify data-listeners (called from the data pump). */
300914
+ notifyTerminalData(terminalId) {
300915
+ for (const listener of this.terminalDataListeners) {
300916
+ try {
300917
+ listener(terminalId);
300918
+ } catch {
300919
+ }
300920
+ }
300921
+ }
300568
300922
  listRenderableTerminals() {
300569
300923
  return Array.from(this.terminals.values()).map((terminal) => ({
300570
300924
  id: terminal.id,
@@ -300883,6 +301237,7 @@ var TerminalService = class _TerminalService {
300883
301237
  }
300884
301238
  this.publishTerminalTabsChanged();
300885
301239
  this.schedulePersistTerminalState();
301240
+ this.notifyTerminalCreated(tab.id);
300886
301241
  return tab;
300887
301242
  }
300888
301243
  async reconnectTerminal(terminalId) {
@@ -301076,6 +301431,7 @@ var TerminalService = class _TerminalService {
301076
301431
  }
301077
301432
  this.sessionLogger.write(terminalId, captureData);
301078
301433
  }
301434
+ this.notifyTerminalData(terminalId);
301079
301435
  });
301080
301436
  }
301081
301437
  }
@@ -301397,6 +301753,7 @@ ${promptPrefix}`;
301397
301753
  const backend = this.getBackend(terminal.type);
301398
301754
  backend.write(terminal.ptyId, data);
301399
301755
  this.pendingWriteRetries.delete(terminalId);
301756
+ this.notifyTerminalData(terminalId);
301400
301757
  } else if (terminal) {
301401
301758
  if (terminal.runtimeState === "exited") {
301402
301759
  this.pendingWrites.delete(terminalId);
@@ -350077,6 +350434,11 @@ var CLOSE_TERMINAL_TAB_DESCRIPTION = [
350077
350434
  "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
350435
  "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
350436
  ].join("\n");
350437
+ var SPAWN_SUBAGENTS_DESCRIPTION = [
350438
+ "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.",
350439
+ "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.",
350440
+ "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."
350441
+ ].join("\n");
350080
350442
  var COPY_BETWEEN_TABS_DESCRIPTION = [
350081
350443
  "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
350444
  "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 +350576,11 @@ var BUILTIN_TOOL_INFO = [
350214
350576
  description: CLOSE_TERMINAL_TAB_DESCRIPTION,
350215
350577
  shortDescription: "Close a terminal tab"
350216
350578
  },
350579
+ {
350580
+ name: "spawn_subagents",
350581
+ description: SPAWN_SUBAGENTS_DESCRIPTION,
350582
+ shortDescription: "Fan out to concurrent child agents"
350583
+ },
350217
350584
  {
350218
350585
  name: "create_or_edit",
350219
350586
  description: CREATE_OR_EDIT_TOOL_DESCRIPTION,
@@ -351869,6 +352236,69 @@ async function closeTerminalTab(args, context2) {
351869
352236
  }
351870
352237
  }
351871
352238
 
352239
+ // ../../packages/backend/src/services/AgentHelper/tools/subAgent_tools.ts
352240
+ init_zod();
352241
+ var spawnSubAgentsSchema = external_exports.object({
352242
+ tasks: external_exports.array(
352243
+ external_exports.object({
352244
+ label: external_exports.string().min(1).describe('Short label for reporting, e.g. "server-1"'),
352245
+ prompt: external_exports.string().min(1).describe("The self-contained prompt for the child agent.")
352246
+ })
352247
+ ).min(1).max(20).describe("The tasks to fan out. Each child gets its own session. Max 20."),
352248
+ maxConcurrent: external_exports.number().int().min(1).max(10).optional().describe("Max children running concurrently (default 3)."),
352249
+ timeoutMs: external_exports.number().int().min(1e3).optional().describe("Per-child timeout in ms (default 300000 = 5 minutes).")
352250
+ });
352251
+ async function spawnSubAgents(args, context2) {
352252
+ abortIfNeeded(context2.signal);
352253
+ const { sendEvent, sessionId, messageId } = context2;
352254
+ sendEvent(sessionId, {
352255
+ messageId,
352256
+ type: "sub_tool_started",
352257
+ toolName: "spawn_subagents",
352258
+ title: `Fan out ${args.tasks.length} sub-agent(s)`,
352259
+ hint: `concurrency ${args.maxConcurrent ?? 3}`,
352260
+ input: JSON.stringify(args)
352261
+ });
352262
+ const finish = (output) => {
352263
+ sendEvent(sessionId, { messageId, type: "sub_tool_delta", outputDelta: output });
352264
+ sendEvent(sessionId, { messageId, type: "sub_tool_finished" });
352265
+ return output;
352266
+ };
352267
+ try {
352268
+ const { validateSubAgentSpec: validateSubAgentSpec2, runSubAgents: runSubAgents2, renderSubAgentSummary: renderSubAgentSummary2 } = await Promise.resolve().then(() => (init_subAgent(), subAgent_exports));
352269
+ const validation = validateSubAgentSpec2(args);
352270
+ if (!validation.ok) {
352271
+ return finish(`Invalid sub-agent spec: ${validation.error}`);
352272
+ }
352273
+ const agentService = context2.agentService;
352274
+ if (!agentService?.startTask) {
352275
+ return finish(
352276
+ "Sub-agent delegation is not available in this runtime (no agent service bridge)."
352277
+ );
352278
+ }
352279
+ const summary = await runSubAgents2(
352280
+ {
352281
+ tasks: args.tasks.map((t) => ({ label: t.label, prompt: t.prompt })),
352282
+ maxConcurrent: args.maxConcurrent,
352283
+ timeoutMs: args.timeoutMs
352284
+ },
352285
+ async (task2) => {
352286
+ const childSession = await agentService.createSession();
352287
+ const result = await agentService.startTask({
352288
+ sessionId: childSession.sessionId,
352289
+ userInput: task2.prompt
352290
+ });
352291
+ const text = typeof result === "string" ? result : String(result?.answer ?? result?.output ?? result);
352292
+ return text;
352293
+ }
352294
+ );
352295
+ return finish(renderSubAgentSummary2(summary));
352296
+ } catch (error40) {
352297
+ if (error40 instanceof Error && error40.name === "AbortError") throw error40;
352298
+ return finish(`Sub-agent fan-out failed: ${error40 instanceof Error ? error40.message : String(error40)}`);
352299
+ }
352300
+ }
352301
+
351872
352302
  // ../../packages/backend/src/services/AgentHelper/tools/wait_tools.ts
351873
352303
  init_zod();
351874
352304
  var waitSchema = external_exports.object({
@@ -354243,6 +354673,11 @@ var CORE_METHODS = [
354243
354673
  m("gateway:isSameMachine", "gateway", "Whether the caller is on the same machine as the backend.", "1.0.0"),
354244
354674
  m("gateway:createSession", "session", "Create an agent session (returns sessionId).", "1.0.0"),
354245
354675
  m("session:list", "session", "List chat sessions.", "1.0.0"),
354676
+ 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"),
354677
+ m("settings:listBackups", "settings", "List timestamped settings backups (v3.2.18). Returns [{name, at, size}] newest-first.", "3.2.18"),
354678
+ 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"),
354679
+ m("settings:export", "settings", "Export the current settings.json content as a portable JSON string (v3.2.18).", "3.2.18"),
354680
+ 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
354681
  m("session:get", "session", "Get one chat session by id.", "1.0.0", { sessionId: { type: "string" } }),
354247
354682
  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
354683
  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 +355647,11 @@ function buildToolsForModel(readFileSupport) {
355212
355647
  description: BUILTIN_TOOL_INFO.find((t) => t.name === "close_terminal_tab")?.description ?? "",
355213
355648
  schema: closeTerminalTabSchema
355214
355649
  },
355650
+ {
355651
+ name: "spawn_subagents",
355652
+ description: BUILTIN_TOOL_INFO.find((t) => t.name === "spawn_subagents")?.description ?? "",
355653
+ schema: spawnSubAgentsSchema
355654
+ },
355215
355655
  {
355216
355656
  name: WRITE_FILE_TOOL_NAME,
355217
355657
  description: WRITE_FILE_TOOL_DESCRIPTION,
@@ -355450,6 +355890,7 @@ var toolImplementations = {
355450
355890
  reconnectTerminalTab,
355451
355891
  openTerminalTab,
355452
355892
  closeTerminalTab,
355893
+ spawnSubAgents,
355453
355894
  wait,
355454
355895
  waitTerminalIdle,
355455
355896
  copyBetweenTabs,
@@ -368370,7 +368811,7 @@ Missing image attachments (kept as references only): ${missingNames.join(", ")}`
368370
368811
  // ../../packages/backend/src/services/PassChatTempExportService.ts
368371
368812
  var import_node_fs4 = __toESM(require("node:fs"), 1);
368372
368813
  var import_promises6 = __toESM(require("node:fs/promises"), 1);
368373
- var import_node_os = __toESM(require("node:os"), 1);
368814
+ var import_node_os2 = __toESM(require("node:os"), 1);
368374
368815
  var import_node_path6 = __toESM(require("node:path"), 1);
368375
368816
  var import_node_crypto15 = __toESM(require("node:crypto"), 1);
368376
368817
  var DEFAULT_MAX_FILES = 50;
@@ -368379,7 +368820,7 @@ var PassChatTempExportService = class {
368379
368820
  maxFiles;
368380
368821
  groupBySession;
368381
368822
  constructor(options) {
368382
- this.baseDir = options?.baseDir || import_node_path6.default.join(import_node_os.default.tmpdir(), "gyshell-pass-chats");
368823
+ this.baseDir = options?.baseDir || import_node_path6.default.join(import_node_os2.default.tmpdir(), "gyshell-pass-chats");
368383
368824
  this.maxFiles = options?.maxFiles === null ? null : Number.isInteger(options?.maxFiles) && Number(options?.maxFiles) > 0 ? Number(options?.maxFiles) : DEFAULT_MAX_FILES;
368384
368825
  this.groupBySession = options?.groupBySession === true;
368385
368826
  }
@@ -369002,6 +369443,20 @@ var AgentService_v2 = class {
369002
369443
  isAbortError(error40) {
369003
369444
  return this.helpers.isAbortError(error40);
369004
369445
  }
369446
+ /**
369447
+ * v3.2.18: resolve a fallback model id to its ModelDefinition from the
369448
+ * session's settings. Returns null when the model isn't configured.
369449
+ */
369450
+ resolveFallbackModelItem(modelId) {
369451
+ try {
369452
+ const settings = this.options?.getSettings?.() ?? null;
369453
+ const items = settings?.models?.items ?? [];
369454
+ const hit = items.find((m2) => m2.model === modelId || m2.id === modelId || m2.name === modelId);
369455
+ return hit ?? null;
369456
+ } catch {
369457
+ return null;
369458
+ }
369459
+ }
369005
369460
  /**
369006
369461
  * v3.2.12: true when the given run's final_output node already emitted the
369007
369462
  * `done` event. GatewayService uses this to avoid broadcasting a duplicate
@@ -369142,6 +369597,22 @@ var AgentService_v2 = class {
369142
369597
  const actionModel = actionItem?.apiKey ? this.helpers.createChatModel(actionItem, 0.1, profile) : model;
369143
369598
  const thinkingModel = thinkingItem?.apiKey ? this.helpers.createChatModel(thinkingItem, 0.2, profile) : model;
369144
369599
  const compactionModel = compactionItem?.apiKey ? this.helpers.createChatModel(compactionItem, 0.2, profile) : thinkingItem?.apiKey ? thinkingModel : model;
369600
+ const fallbackModels = [];
369601
+ const seenFallbackIds = /* @__PURE__ */ new Set([profile.globalModelId]);
369602
+ for (const fallbackId of Array.isArray(profile.fallbackModels) ? profile.fallbackModels : []) {
369603
+ if (typeof fallbackId !== "string" || !fallbackId.trim()) continue;
369604
+ if (seenFallbackIds.has(fallbackId)) continue;
369605
+ const item = settings.models.items.find((m2) => m2.id === fallbackId);
369606
+ if (!item || !item.apiKey) {
369607
+ console.warn(
369608
+ "[AgentService_v2] Fallback model is invalid or has no API key \u2014 skipping:",
369609
+ { profileId, fallbackId }
369610
+ );
369611
+ continue;
369612
+ }
369613
+ seenFallbackIds.add(fallbackId);
369614
+ fallbackModels.push({ model: item.model, label: item.name ?? item.model });
369615
+ }
369145
369616
  const actionModelSupportsStructuredOutput = actionItem?.apiKey ? actionItem.supportsStructuredOutput === true : globalItem.supportsStructuredOutput === true;
369146
369617
  const actionModelSupportsObjectToolChoice = actionItem?.apiKey ? actionItem.supportsObjectToolChoice === true : globalItem.supportsObjectToolChoice === true;
369147
369618
  const thinkingModelSupportsStructuredOutput = thinkingItem?.apiKey ? thinkingItem.supportsStructuredOutput === true : globalItem.supportsStructuredOutput === true;
@@ -369162,6 +369633,7 @@ var AgentService_v2 = class {
369162
369633
  actionModel,
369163
369634
  thinkingModel,
369164
369635
  compactionModel,
369636
+ fallbackModels,
369165
369637
  actionModelSupportsStructuredOutput,
369166
369638
  actionModelSupportsObjectToolChoice,
369167
369639
  thinkingModelSupportsStructuredOutput,
@@ -369549,172 +370021,231 @@ var AgentService_v2 = class {
369549
370021
  let reasoningContent = "";
369550
370022
  let debugRawChunks = [];
369551
370023
  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
370024
+ const fallbacks = sessionBinding.fallbackModels;
370025
+ let fullResponse;
370026
+ if (Array.isArray(fallbacks) && fallbacks.length > 0) {
370027
+ const { withModelFailover: withModelFailover2 } = await Promise.resolve().then(() => (init_modelFailover(), modelFailover_exports));
370028
+ const primaryModel = baseModel?.model ?? baseModel?.modelName ?? "unknown";
370029
+ const chain = [{ model: primaryModel }, ...fallbacks.map((f) => ({ model: f.model, label: f.label }))];
370030
+ const failover = await withModelFailover2(chain, async (candidate) => {
370031
+ let modelToUse = modelWithTools;
370032
+ if (candidate.model !== primaryModel) {
370033
+ const fallbackItem = this.resolveFallbackModelItem(candidate.model);
370034
+ if (!fallbackItem) {
370035
+ throw new Error(`fallback model "${candidate.model}" is not configured`);
370036
+ }
370037
+ const fallbackChat = this.helpers.createChatModel(
370038
+ fallbackItem,
370039
+ shouldUseThinkingModelOnThisPass ? 0.2 : 0.7,
370040
+ null
370041
+ );
370042
+ modelToUse = fallbackChat.bindTools([...builtInTools, ...mcpTools]);
370043
+ }
370044
+ return await invokeWithRetryAndSanitizedInput({
370045
+ helpers: this.helpers,
370046
+ messages: modelInputMessages,
370047
+ modelSupportsImage: sessionBinding.readFileSupport.image,
370048
+ signal: config2?.signal,
370049
+ operation: async (streamInputMessages) => {
370050
+ const stream = await modelToUse.stream(streamInputMessages, { signal: config2?.signal });
370051
+ let response = null;
370052
+ for await (const chunk2 of stream) {
370053
+ response = response ? response.concat(chunk2) : chunk2;
370054
+ }
370055
+ if (!response) throw new Error("Model stream ended without a usable response.");
370056
+ return response;
370057
+ },
370058
+ onRetry: (attempt) => {
370059
+ this.helpers.sendEvent(sessionId, {
370060
+ type: "alert",
370061
+ message: `Retrying (${attempt}/${MODEL_RETRY_MAX})...`,
370062
+ level: "info",
370063
+ messageId: `retry-${messageId}-${attempt}`
370064
+ });
370065
+ },
370066
+ maxRetries: MODEL_RETRY_MAX,
370067
+ delaysMs: MODEL_RETRY_DELAYS_MS
369560
370068
  });
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"
370069
+ }, {
370070
+ onAttempt: (a) => {
370071
+ if (!a.ok) {
370072
+ console.warn(`[AgentService_v2] model failover: ${a.model} failed (${a.reason}) after ${a.durationMs}ms`);
370073
+ }
370074
+ }
370075
+ });
370076
+ if (failover.value !== void 0) {
370077
+ fullResponse = failover.value;
370078
+ } else {
370079
+ throw failover.error ?? new Error("model failover exhausted all candidates");
370080
+ }
370081
+ } else {
370082
+ fullResponse = await invokeWithRetryAndSanitizedInput({
370083
+ helpers: this.helpers,
370084
+ messages: modelInputMessages,
370085
+ modelSupportsImage: sessionBinding.readFileSupport.image,
370086
+ signal: config2?.signal,
370087
+ operation: async (streamInputMessages) => {
370088
+ const stream = await modelWithTools.stream(streamInputMessages, {
370089
+ signal: config2?.signal
369590
370090
  });
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();
370091
+ let response = null;
370092
+ let skippedEmptyGenericChunks = 0;
370093
+ const streamReasoningExtractor = createStreamReasoningExtractor();
370094
+ const attemptDebugRawChunks = [];
370095
+ let activeReasoningBannerId = null;
370096
+ const startReasoningBanner = () => {
370097
+ if (activeReasoningBannerId) return;
370098
+ activeReasoningBannerId = v4_default();
370099
+ this.helpers.sendEvent(sessionId, {
370100
+ messageId: activeReasoningBannerId,
370101
+ type: "sub_tool_started",
370102
+ title: "Reasoning...",
370103
+ hint: ""
370104
+ });
370105
+ };
370106
+ const appendReasoningDelta = (delta) => {
370107
+ if (!delta) return;
370108
+ startReasoningBanner();
370109
+ this.helpers.sendEvent(sessionId, {
370110
+ messageId: activeReasoningBannerId,
370111
+ type: "sub_tool_delta",
370112
+ outputDelta: delta
370113
+ });
370114
+ };
370115
+ const finishReasoningBanner = () => {
370116
+ if (!activeReasoningBannerId) return;
370117
+ this.helpers.sendEvent(sessionId, {
370118
+ messageId: activeReasoningBannerId,
370119
+ type: "sub_tool_finished"
370120
+ });
370121
+ activeReasoningBannerId = null;
370122
+ };
370123
+ try {
370124
+ for await (const chunk2 of stream) {
370125
+ const rawChunk = captureRawResponseChunk(
370126
+ chunk2,
370127
+ attemptDebugRawChunks
370128
+ );
370129
+ const extracted = streamReasoningExtractor.processChunk(
370130
+ chunk2,
370131
+ rawChunk
370132
+ );
370133
+ const appendResult = appendStreamedModelResponseChunk(
370134
+ response,
370135
+ chunk2,
370136
+ rawChunk
370137
+ );
370138
+ response = appendResult.response;
370139
+ if (appendResult.skippedEmptyGenericChunk) {
370140
+ skippedEmptyGenericChunks += 1;
370141
+ }
370142
+ const rawDelta = this.helpers.extractText(chunk2.content);
370143
+ if (rawDelta) {
370144
+ partialText += rawDelta;
370145
+ }
370146
+ if (extracted.reasoning) {
370147
+ appendReasoningDelta(extracted.reasoning);
370148
+ } else {
370149
+ finishReasoningBanner();
370150
+ }
370151
+ if (extracted.content) {
370152
+ this.helpers.sendEvent(sessionId, {
370153
+ messageId,
370154
+ type: "say",
370155
+ content: extracted.content
370156
+ });
370157
+ }
369620
370158
  }
369621
- if (extracted.content) {
370159
+ const pendingContent = streamReasoningExtractor.flushPendingContent();
370160
+ if (pendingContent) {
369622
370161
  this.helpers.sendEvent(sessionId, {
369623
370162
  messageId,
369624
370163
  type: "say",
369625
- content: extracted.content
370164
+ content: pendingContent
369626
370165
  });
369627
370166
  }
370167
+ finishReasoningBanner();
370168
+ } catch (err) {
370169
+ finishReasoningBanner();
370170
+ if (partialText.trim()) {
370171
+ this.lastAbortedMessage = new AIMessage({
370172
+ content: partialText,
370173
+ additional_kwargs: {
370174
+ _gyshellMessageId: messageId,
370175
+ _gyshellAborted: true
370176
+ }
370177
+ });
370178
+ console.log(
370179
+ "[AgentService_v2] Captured partial message from error/abort in instance variable."
370180
+ );
370181
+ }
370182
+ throw err;
369628
370183
  }
369629
- const pendingContent = streamReasoningExtractor.flushPendingContent();
369630
- if (pendingContent) {
369631
- this.helpers.sendEvent(sessionId, {
369632
- messageId,
369633
- type: "say",
369634
- content: pendingContent
369635
- });
370184
+ if (!response) {
370185
+ throw new Error("Model stream ended without a usable response.");
369636
370186
  }
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
370187
+ if (skippedEmptyGenericChunks > 0) {
370188
+ response.additional_kwargs = {
370189
+ ...response.additional_kwargs || {},
370190
+ [SKIPPED_EMPTY_GENERIC_CHUNKS_KEY]: skippedEmptyGenericChunks
370191
+ };
370192
+ }
370193
+ reasoningContent = streamReasoningExtractor.getReasoningContent();
370194
+ if (isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && typeof modelWithTools.invoke === "function") {
370195
+ console.warn(
370196
+ `[AgentService_v2] Stream ended with malformed empty tool-call finish; retrying same request with non-stream invoke (sessionId=${sessionId}).`
370197
+ );
370198
+ const invokeResponse = await modelWithTools.invoke(
370199
+ streamInputMessages,
370200
+ {
370201
+ signal: config2?.signal
369646
370202
  }
369647
- });
369648
- console.log(
369649
- "[AgentService_v2] Captured partial message from error/abort in instance variable."
369650
370203
  );
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
370204
+ captureRawResponseChunk(
370205
+ invokeResponse,
370206
+ attemptDebugRawChunks
369684
370207
  );
369685
- if (invokeText) {
369686
- this.helpers.sendEvent(sessionId, {
369687
- messageId,
369688
- type: "say",
369689
- content: invokeText
369690
- });
370208
+ if (!isEmptyMalformedToolCallFinish(
370209
+ invokeResponse,
370210
+ attemptDebugRawChunks
370211
+ )) {
370212
+ const invokeText = this.helpers.extractText(
370213
+ invokeResponse.content
370214
+ );
370215
+ if (invokeText) {
370216
+ this.helpers.sendEvent(sessionId, {
370217
+ messageId,
370218
+ type: "say",
370219
+ content: invokeText
370220
+ });
370221
+ }
370222
+ response = invokeResponse;
369691
370223
  }
369692
- response = invokeResponse;
369693
370224
  }
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
- });
370225
+ if (!isEmptyMalformedToolCallFinish(response, attemptDebugRawChunks) && isEmptyUnusableModelResponse(response, attemptDebugRawChunks)) {
370226
+ const finishReason = describeStreamedResponseFinish(
370227
+ response,
370228
+ attemptDebugRawChunks
370229
+ );
370230
+ throw new Error(
370231
+ `Model stream ended with an empty unusable response (finish_reason=${finishReason}).`
370232
+ );
370233
+ }
370234
+ debugRawChunks = attemptDebugRawChunks;
370235
+ return response;
370236
+ },
370237
+ onRetry: (attempt) => {
370238
+ this.helpers.sendEvent(sessionId, {
370239
+ type: "alert",
370240
+ message: `Retrying (${attempt}/${MODEL_RETRY_MAX})...`,
370241
+ level: "info",
370242
+ messageId: `retry-${messageId}-${attempt}`
370243
+ });
370244
+ },
370245
+ maxRetries: MODEL_RETRY_MAX,
370246
+ delaysMs: MODEL_RETRY_DELAYS_MS
370247
+ });
370248
+ }
369718
370249
  fullResponse.additional_kwargs = {
369719
370250
  ...fullResponse.additional_kwargs || {},
369720
370251
  _gyshellMessageId: messageId
@@ -370110,6 +370641,22 @@ Actually, your intention might be different. Please re-read the description of t
370110
370641
  }
370111
370642
  break;
370112
370643
  }
370644
+ case "spawn_subagents": {
370645
+ try {
370646
+ const validatedArgs = spawnSubAgentsSchema.parse(
370647
+ toolCall.args || {}
370648
+ );
370649
+ result = await toolImplementations.spawnSubAgents(
370650
+ validatedArgs,
370651
+ // The sub-agent tool needs the agent service to create child
370652
+ // sessions — inject it into the execution context.
370653
+ { ...executionContext, agentService: this }
370654
+ );
370655
+ } catch (err) {
370656
+ result = `Parameter validation error for spawn_subagents: ${err.message}`;
370657
+ }
370658
+ break;
370659
+ }
370113
370660
  case "manage_ssh_connection": {
370114
370661
  try {
370115
370662
  const validatedArgs = manageSshConnectionSchema.parse(
@@ -374652,13 +375199,16 @@ var WebSocketGatewayAdapter = class {
374652
375199
  }
374653
375200
  const token = this.extractAccessToken(request);
374654
375201
  if (!token) {
375202
+ this.options.rateLimiter?.recordAuthFailure(this.clientKeyForRequest(request));
374655
375203
  return "missing access token";
374656
375204
  }
374657
375205
  try {
374658
375206
  const valid = await auth2.verifyToken(token);
374659
375207
  if (!valid) {
375208
+ this.options.rateLimiter?.recordAuthFailure(this.clientKeyForRequest(request));
374660
375209
  return "invalid access token";
374661
375210
  }
375211
+ this.options.rateLimiter?.recordAuthSuccess(this.clientKeyForRequest(request));
374662
375212
  return null;
374663
375213
  } catch (error40) {
374664
375214
  this.logger.warn(
@@ -374668,6 +375218,15 @@ var WebSocketGatewayAdapter = class {
374668
375218
  return "token verification failed";
374669
375219
  }
374670
375220
  }
375221
+ /** v3.2.18: a stable per-client key for rate limiting (IP or token id). */
375222
+ clientKeyForRequest(request) {
375223
+ return String(request?.socket?.remoteAddress || "unknown");
375224
+ }
375225
+ /** v3.2.18: per-client key from a live socket connection. */
375226
+ clientKeyForSocket(socket) {
375227
+ const remote = socket.remoteAddress;
375228
+ return String(remote || "unknown");
375229
+ }
374671
375230
  extractAccessToken(request) {
374672
375231
  const fromAuthHeader = this.readBearerToken(
374673
375232
  request?.headers?.authorization
@@ -374828,6 +375387,17 @@ var WebSocketGatewayAdapter = class {
374828
375387
  try {
374829
375388
  const parsed = this.parseRequest(raw);
374830
375389
  requestId = parsed.id !== void 0 ? String(parsed.id) : void 0;
375390
+ const limiter = this.options.rateLimiter;
375391
+ if (limiter) {
375392
+ const clientKey = this.clientKeyForSocket(socket);
375393
+ const decision = limiter.check(clientKey);
375394
+ if (!decision.allowed) {
375395
+ throw new WebSocketRpcError(
375396
+ "RATE_LIMITED",
375397
+ 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`
375398
+ );
375399
+ }
375400
+ }
374831
375401
  const result = await this.executeRequest(parsed, socket);
374832
375402
  if (requestId) {
374833
375403
  this.sendRpcSuccess(socket, requestId, result);
@@ -374885,6 +375455,26 @@ var WebSocketGatewayAdapter = class {
374885
375455
  "Unsupported websocket payload type."
374886
375456
  );
374887
375457
  }
375458
+ /**
375459
+ * v3.2.18: public dispatch for non-WebSocket callers (the REST API layer).
375460
+ * Executes a gateway method with a synthetic loopback socket — same
375461
+ * validation and error mapping as a WS request, no socket required.
375462
+ */
375463
+ async handleRequest(method, params) {
375464
+ const syntheticSocket = {
375465
+ send: () => {
375466
+ },
375467
+ close: () => {
375468
+ },
375469
+ on: () => {
375470
+ },
375471
+ readyState: 1
375472
+ };
375473
+ return await this.executeRequest(
375474
+ { id: `rest-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, method, params },
375475
+ syntheticSocket
375476
+ );
375477
+ }
374888
375478
  async executeRequest(request, socket) {
374889
375479
  const params = request.params ?? {};
374890
375480
  if (request.method.startsWith("observability:")) {
@@ -374961,6 +375551,61 @@ var WebSocketGatewayAdapter = class {
374961
375551
  }
374962
375552
  return { session };
374963
375553
  }
375554
+ case "history:search": {
375555
+ const bridge = this.options.historyBridge;
375556
+ if (!bridge?.getAllSessions) {
375557
+ throw new WebSocketRpcError(
375558
+ "METHOD_NOT_FOUND",
375559
+ "history:search is not available on this gateway (no history bridge)."
375560
+ );
375561
+ }
375562
+ const { searchChatHistory: searchChatHistory2 } = await Promise.resolve().then(() => (init_historySearch(), historySearch_exports));
375563
+ const sessions = await bridge.getAllSessions();
375564
+ const query = this.readStringParam(params, "query");
375565
+ const wholeWord = params?.wholeWord === true;
375566
+ const includeTitles = params?.includeTitles !== false;
375567
+ const sessionLimit = typeof params?.sessionLimit === "number" ? params.sessionLimit : void 0;
375568
+ const snippetLimit = typeof params?.snippetLimit === "number" ? params.snippetLimit : void 0;
375569
+ return searchChatHistory2(sessions, query, { wholeWord, includeTitles, sessionLimit, snippetLimit });
375570
+ }
375571
+ case "settings:listBackups": {
375572
+ const bridge = this.options.settingsBridge;
375573
+ if (!bridge?.listBackups) {
375574
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings backups are not available on this gateway.");
375575
+ }
375576
+ return { backups: await bridge.listBackups() };
375577
+ }
375578
+ case "settings:restoreBackup": {
375579
+ const bridge = this.options.settingsBridge;
375580
+ if (!bridge?.restoreBackup) {
375581
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings backups are not available on this gateway.");
375582
+ }
375583
+ const name = this.readStringParam(params, "name");
375584
+ const result = await bridge.restoreBackup(name);
375585
+ if (result && typeof result === "object" && "ok" in result && !result.ok) {
375586
+ throw new WebSocketRpcError("BAD_REQUEST", String(result.error ?? "restore failed"));
375587
+ }
375588
+ return { restored: name };
375589
+ }
375590
+ case "settings:export": {
375591
+ const bridge = this.options.settingsBridge;
375592
+ if (!bridge?.export) {
375593
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings export is not available on this gateway.");
375594
+ }
375595
+ return { content: await bridge.export() };
375596
+ }
375597
+ case "settings:import": {
375598
+ const bridge = this.options.settingsBridge;
375599
+ if (!bridge?.import) {
375600
+ throw new WebSocketRpcError("METHOD_NOT_FOUND", "settings import is not available on this gateway.");
375601
+ }
375602
+ const content = this.readStringParam(params, "content");
375603
+ const result = await bridge.import(content);
375604
+ if (result && typeof result === "object" && "ok" in result && !result.ok) {
375605
+ throw new WebSocketRpcError("BAD_REQUEST", String(result.error ?? "import failed"));
375606
+ }
375607
+ return { imported: true };
375608
+ }
374964
375609
  case "agent:exportHistory": {
374965
375610
  const bridge = this.options.agentBridge;
374966
375611
  if (!bridge?.exportHistory) {
@@ -383611,7 +384256,7 @@ var NodeMcpToolService = class extends McpRuntimeCore {
383611
384256
 
383612
384257
  // ../../packages/backend/src/adapters/node/NodeSkillService.ts
383613
384258
  var import_node_path16 = __toESM(require("node:path"), 1);
383614
- var import_node_os3 = __toESM(require("node:os"), 1);
384259
+ var import_node_os4 = __toESM(require("node:os"), 1);
383615
384260
 
383616
384261
  // ../../packages/backend/src/skills/FileSkillStore.ts
383617
384262
  var import_promises9 = __toESM(require("node:fs/promises"), 1);
@@ -383916,10 +384561,10 @@ ${files}`;
383916
384561
 
383917
384562
  // ../../packages/backend/src/skills/scanRoots.ts
383918
384563
  var import_node_path15 = __toESM(require("node:path"), 1);
383919
- var import_node_os2 = __toESM(require("node:os"), 1);
384564
+ var import_node_os3 = __toESM(require("node:os"), 1);
383920
384565
  function resolveDefaultSkillScanRoots(options) {
383921
384566
  const primaryRoot = import_node_path15.default.resolve(options.primaryRoot);
383922
- const homeDir = (options.homeDir || import_node_os2.default.homedir() || "").trim();
384567
+ const homeDir = (options.homeDir || import_node_os3.default.homedir() || "").trim();
383923
384568
  const platform2 = options.platform || process.platform;
383924
384569
  const appData = (options.appData || process.env.APPDATA || "").trim();
383925
384570
  const codexHome = (options.codexHome || process.env.CODEX_HOME || "").trim();
@@ -383991,7 +384636,7 @@ var NodeSkillService = class {
383991
384636
  resolveScanRoots() {
383992
384637
  return resolveDefaultSkillScanRoots({
383993
384638
  primaryRoot: this.skillsDir,
383994
- homeDir: import_node_os3.default.homedir(),
384639
+ homeDir: import_node_os4.default.homedir(),
383995
384640
  platform: process.platform,
383996
384641
  appData: process.env.APPDATA,
383997
384642
  codexHome: process.env.CODEX_HOME
@@ -386114,6 +386759,367 @@ var SessionLogService = class _SessionLogService {
386114
386759
 
386115
386760
  // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
386116
386761
  init_schedulerService();
386762
+
386763
+ // ../../packages/backend/src/services/settings/settingsBackup.ts
386764
+ var BACKUP_PREFIX = "settings-backup-";
386765
+ var BACKUP_SUFFIX = ".json";
386766
+ function backupNameFor(date6) {
386767
+ const pad = (n2, w = 2) => String(n2).padStart(w, "0");
386768
+ return `${BACKUP_PREFIX}${date6.getUTCFullYear()}${pad(date6.getUTCMonth() + 1)}${pad(date6.getUTCDate())}-${pad(date6.getUTCHours())}${pad(date6.getUTCMinutes())}${pad(date6.getUTCSeconds())}${BACKUP_SUFFIX}`;
386769
+ }
386770
+ function parseBackupName(name) {
386771
+ if (!name.startsWith(BACKUP_PREFIX) || !name.endsWith(BACKUP_SUFFIX)) return null;
386772
+ const stem = name.slice(BACKUP_PREFIX.length, -BACKUP_SUFFIX.length);
386773
+ const m2 = stem.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/);
386774
+ if (!m2) return null;
386775
+ const [, y, mo, d, h, mi, s] = m2.map(Number);
386776
+ const date6 = new Date(Date.UTC(y, mo - 1, d, h, mi, s));
386777
+ return Number.isNaN(date6.getTime()) ? null : date6;
386778
+ }
386779
+ var SettingsBackupService = class {
386780
+ constructor(io, opts = {}) {
386781
+ this.io = io;
386782
+ this.keep = opts.keep ?? 20;
386783
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
386784
+ }
386785
+ keep;
386786
+ now;
386787
+ /** Take a backup of the CURRENT settings content, then rotate. */
386788
+ backup() {
386789
+ const content = this.io.read();
386790
+ const name = backupNameFor(this.now());
386791
+ this.io.writeBackup(name, content);
386792
+ this.rotate();
386793
+ return { name, at: this.now(), size: content.length };
386794
+ }
386795
+ /** Delete oldest backups beyond the keep limit. Returns names removed. */
386796
+ rotate() {
386797
+ 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());
386798
+ const excess = names.slice(0, Math.max(0, names.length - this.keep));
386799
+ for (const { n: n2 } of excess) this.io.deleteBackup(n2);
386800
+ return excess.map((x) => x.n);
386801
+ }
386802
+ /** List backups, newest first. */
386803
+ list() {
386804
+ 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 }));
386805
+ }
386806
+ /** Restore a backup by name (writes it to the live settings path). */
386807
+ restore(name) {
386808
+ if (!this.io.listBackups().includes(name)) {
386809
+ return { ok: false, error: `no backup named "${name}"` };
386810
+ }
386811
+ this.backup();
386812
+ const content = this.io.readBackup(name);
386813
+ this.io.write(content);
386814
+ return { ok: true };
386815
+ }
386816
+ /** Export the current settings as a portable JSON string. */
386817
+ export() {
386818
+ return this.io.read();
386819
+ }
386820
+ /** Validate + import settings JSON. Refuses non-object / empty content. */
386821
+ import(content) {
386822
+ const trimmed = (content ?? "").trim();
386823
+ if (!trimmed) return { ok: false, error: "empty settings content" };
386824
+ let parsed;
386825
+ try {
386826
+ parsed = JSON.parse(trimmed);
386827
+ } catch (e) {
386828
+ return { ok: false, error: `invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
386829
+ }
386830
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
386831
+ return { ok: false, error: "settings content must be a JSON object" };
386832
+ }
386833
+ this.backup();
386834
+ this.io.write(trimmed);
386835
+ return { ok: true };
386836
+ }
386837
+ };
386838
+
386839
+ // ../../packages/backend/src/services/terminal/idleTimeout.ts
386840
+ var IdleTimeoutService = class {
386841
+ idleMs;
386842
+ protectedIds;
386843
+ now;
386844
+ lastActivity = /* @__PURE__ */ new Map();
386845
+ constructor(opts = {}) {
386846
+ this.idleMs = Math.max(0, (opts.idleMinutes ?? 30) * 6e4);
386847
+ this.protectedIds = new Set(opts.protectedIds ?? []);
386848
+ this.now = opts.now ?? (() => Date.now());
386849
+ }
386850
+ /** Record activity for a terminal. */
386851
+ touch(terminalId) {
386852
+ this.lastActivity.set(terminalId, this.now());
386853
+ }
386854
+ /** Register a terminal (starts its idle clock). */
386855
+ register(terminalId) {
386856
+ if (!this.lastActivity.has(terminalId)) {
386857
+ this.lastActivity.set(terminalId, this.now());
386858
+ }
386859
+ }
386860
+ /** Forget a terminal (on close). */
386861
+ forget(terminalId) {
386862
+ this.lastActivity.delete(terminalId);
386863
+ }
386864
+ /** Is this terminal idle past the threshold? */
386865
+ isIdle(terminalId) {
386866
+ if (this.protectedIds.has(terminalId)) return false;
386867
+ const last = this.lastActivity.get(terminalId);
386868
+ if (last === void 0) return false;
386869
+ return this.now() - last >= this.idleMs;
386870
+ }
386871
+ /** All currently-idle terminals (among the known ones). */
386872
+ idleTerminals(knownIds) {
386873
+ const t = this.now();
386874
+ const out = [];
386875
+ for (const id of knownIds) {
386876
+ if (this.protectedIds.has(id)) continue;
386877
+ const last = this.lastActivity.get(id);
386878
+ if (last === void 0) continue;
386879
+ const idleMin = (t - last) / 6e4;
386880
+ if (idleMin * 6e4 >= this.idleMs) {
386881
+ out.push({ terminalId: id, idleMinutes: Math.round(idleMin * 10) / 10 });
386882
+ }
386883
+ }
386884
+ return out;
386885
+ }
386886
+ /** Configure the threshold at runtime. */
386887
+ setIdleMinutes(minutes) {
386888
+ this.idleMs = Math.max(0, minutes * 6e4);
386889
+ }
386890
+ get idleMinutes() {
386891
+ return this.idleMs / 6e4;
386892
+ }
386893
+ };
386894
+
386895
+ // ../../packages/backend/src/services/Gateway/restApi.ts
386896
+ function matchRestRoute(routes, method, path32) {
386897
+ const normalized = path32.replace(/\/+$/, "") || "/";
386898
+ for (const route of routes) {
386899
+ if (route.method !== method.toUpperCase()) continue;
386900
+ const patternParts = route.path.split("/").filter(Boolean);
386901
+ const pathParts = normalized.split("/").filter(Boolean);
386902
+ if (patternParts.length !== pathParts.length) continue;
386903
+ const params = {};
386904
+ let matched = true;
386905
+ for (let i = 0; i < patternParts.length; i++) {
386906
+ const p = patternParts[i];
386907
+ if (p.startsWith(":")) {
386908
+ params[p.slice(1)] = decodeURIComponent(pathParts[i]);
386909
+ } else if (p !== pathParts[i]) {
386910
+ matched = false;
386911
+ break;
386912
+ }
386913
+ }
386914
+ if (matched) return { route, pathParams: params };
386915
+ }
386916
+ return null;
386917
+ }
386918
+ function defaultRestRoutes() {
386919
+ return [
386920
+ {
386921
+ method: "GET",
386922
+ path: "/api/v1/health",
386923
+ gatewayMethod: "gateway:ping",
386924
+ description: "Liveness check"
386925
+ },
386926
+ {
386927
+ method: "GET",
386928
+ path: "/api/v1/methods",
386929
+ gatewayMethod: "gateway:describe",
386930
+ description: "List all gateway methods (self-describing)"
386931
+ },
386932
+ {
386933
+ method: "GET",
386934
+ path: "/api/v1/terminals",
386935
+ gatewayMethod: "terminal:list",
386936
+ description: "List terminal tabs"
386937
+ },
386938
+ {
386939
+ method: "GET",
386940
+ path: "/api/v1/sessions",
386941
+ gatewayMethod: "session:list",
386942
+ description: "List chat sessions"
386943
+ },
386944
+ {
386945
+ method: "GET",
386946
+ path: "/api/v1/skills",
386947
+ gatewayMethod: "skills:getAll",
386948
+ description: "List loaded skills"
386949
+ },
386950
+ {
386951
+ method: "GET",
386952
+ path: "/api/v1/observability/metrics",
386953
+ gatewayMethod: "observability:metricsPrometheus",
386954
+ description: "Host metrics (Prometheus text or summary)"
386955
+ },
386956
+ {
386957
+ method: "GET",
386958
+ path: "/api/v1/observability/dashboard",
386959
+ gatewayMethod: "observability:liveDashboardState",
386960
+ description: "Live dashboard state"
386961
+ },
386962
+ {
386963
+ method: "GET",
386964
+ path: "/api/v1/observability/apm",
386965
+ gatewayMethod: "observability:apmSummary",
386966
+ description: "APM summary (LLM + app traces)"
386967
+ },
386968
+ {
386969
+ method: "GET",
386970
+ path: "/api/v1/history/search",
386971
+ gatewayMethod: "history:search",
386972
+ description: "Cross-session history search (?q=...)",
386973
+ buildParams: (_p, body) => {
386974
+ const b = body ?? {};
386975
+ return { query: b.q ?? b.query ?? "" };
386976
+ }
386977
+ },
386978
+ {
386979
+ method: "POST",
386980
+ path: "/api/v1/terminals/:id/write",
386981
+ gatewayMethod: "terminal:write",
386982
+ description: "Write data to a terminal tab",
386983
+ buildParams: (p, body) => {
386984
+ const b = body ?? {};
386985
+ return { terminalId: p.id, data: b.data ?? "" };
386986
+ }
386987
+ },
386988
+ {
386989
+ method: "GET",
386990
+ path: "/api/v1/terminals/:id/buffer",
386991
+ gatewayMethod: "terminal:getBufferDelta",
386992
+ description: "Read a terminal tab output delta",
386993
+ buildParams: (p, body) => {
386994
+ const b = body ?? {};
386995
+ return { terminalId: p.id, fromOffset: b.fromOffset ?? 0 };
386996
+ }
386997
+ },
386998
+ {
386999
+ method: "POST",
387000
+ path: "/api/v1/sessions/:id/chat",
387001
+ gatewayMethod: "agent:startTask",
387002
+ description: "Send a message to the agent (blocking)",
387003
+ buildParams: (p, body) => {
387004
+ const b = body ?? {};
387005
+ return { sessionId: p.id, userInput: b.message ?? b.userInput ?? "" };
387006
+ }
387007
+ },
387008
+ {
387009
+ method: "POST",
387010
+ path: "/api/v1/rpc",
387011
+ gatewayMethod: "",
387012
+ description: "Escape hatch: dispatch any gateway method",
387013
+ buildParams: (_p, body) => {
387014
+ const b = body ?? {};
387015
+ return { __rpcMethod: b.method ?? "", ...b.params ?? {} };
387016
+ }
387017
+ }
387018
+ ];
387019
+ }
387020
+ async function handleRestRequest(routes, dispatch, req) {
387021
+ const match = matchRestRoute(routes, req.method, req.path);
387022
+ if (!match) {
387023
+ return {
387024
+ status: 404,
387025
+ body: { error: "not_found", message: `No REST route for ${req.method} ${req.path}` }
387026
+ };
387027
+ }
387028
+ let gatewayMethod = match.route.gatewayMethod;
387029
+ let params;
387030
+ if (gatewayMethod === "") {
387031
+ const raw = req.body ?? {};
387032
+ if (!raw.method) {
387033
+ return { status: 400, body: { error: "bad_request", message: 'POST /api/v1/rpc needs {"method": "...", "params": {...}}' } };
387034
+ }
387035
+ gatewayMethod = raw.method;
387036
+ params = raw.params ?? {};
387037
+ } else {
387038
+ params = match.route.buildParams?.(match.pathParams, req.body) ?? match.pathParams;
387039
+ }
387040
+ try {
387041
+ const result = await dispatch(gatewayMethod, params);
387042
+ return { status: 200, body: result ?? { ok: true } };
387043
+ } catch (error40) {
387044
+ const message = error40 instanceof Error ? error40.message : String(error40);
387045
+ 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;
387046
+ return { status, body: { error: "gateway_error", message } };
387047
+ }
387048
+ }
387049
+
387050
+ // ../../packages/backend/src/services/Gateway/gatewayRateLimit.ts
387051
+ var GatewayRateLimiter = class {
387052
+ capacity;
387053
+ refillPerSecond;
387054
+ authFailureLimit;
387055
+ authLockoutMs;
387056
+ now;
387057
+ buckets = /* @__PURE__ */ new Map();
387058
+ authTrackers = /* @__PURE__ */ new Map();
387059
+ constructor(opts = {}) {
387060
+ this.capacity = opts.capacity ?? 60;
387061
+ this.refillPerSecond = opts.refillPerSecond ?? 1;
387062
+ this.authFailureLimit = opts.authFailureLimit ?? 5;
387063
+ this.authLockoutMs = opts.authLockoutMs ?? 6e4;
387064
+ this.now = opts.now ?? (() => Date.now());
387065
+ }
387066
+ refill(bucket) {
387067
+ const t = this.now();
387068
+ const elapsed2 = (t - bucket.lastRefill) / 1e3;
387069
+ if (elapsed2 <= 0) return;
387070
+ bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed2 * this.refillPerSecond);
387071
+ bucket.lastRefill = t;
387072
+ }
387073
+ /** Check (and consume) one token for a client. */
387074
+ check(clientKey) {
387075
+ const t = this.now();
387076
+ const bucket = this.buckets.get(clientKey) ?? { tokens: this.capacity, lastRefill: t };
387077
+ this.refill(bucket);
387078
+ this.buckets.set(clientKey, bucket);
387079
+ if (bucket.tokens >= 1) {
387080
+ bucket.tokens -= 1;
387081
+ return { allowed: true, remaining: Math.floor(bucket.tokens) };
387082
+ }
387083
+ const msToNextToken = Math.ceil((1 - bucket.tokens) / this.refillPerSecond * 1e3);
387084
+ return { allowed: false, remaining: 0, retryAfterMs: msToNextToken, reason: "rate-limited" };
387085
+ }
387086
+ /** Record a failed authentication attempt; returns the lockout decision. */
387087
+ recordAuthFailure(clientKey) {
387088
+ const t = this.now();
387089
+ const tracker = this.authTrackers.get(clientKey) ?? { failures: 0, lockedUntil: 0 };
387090
+ tracker.failures += 1;
387091
+ if (tracker.failures >= this.authFailureLimit) {
387092
+ tracker.lockedUntil = t + this.authLockoutMs;
387093
+ tracker.failures = 0;
387094
+ }
387095
+ this.authTrackers.set(clientKey, tracker);
387096
+ if (t < tracker.lockedUntil) {
387097
+ return { allowed: false, remaining: 0, retryAfterMs: tracker.lockedUntil - t, reason: "locked-out" };
387098
+ }
387099
+ return { allowed: true, remaining: this.authFailureLimit - tracker.failures };
387100
+ }
387101
+ /** Clear auth failures on successful authentication. */
387102
+ recordAuthSuccess(clientKey) {
387103
+ this.authTrackers.delete(clientKey);
387104
+ }
387105
+ /** Is this client currently locked out? */
387106
+ isLockedOut(clientKey) {
387107
+ const tracker = this.authTrackers.get(clientKey);
387108
+ if (!tracker) return false;
387109
+ return this.now() < tracker.lockedUntil;
387110
+ }
387111
+ /** Drop state for a client (on disconnect). */
387112
+ forget(clientKey) {
387113
+ this.buckets.delete(clientKey);
387114
+ this.authTrackers.delete(clientKey);
387115
+ }
387116
+ /** Number of tracked clients (for monitoring/tests). */
387117
+ get clientCount() {
387118
+ return this.buckets.size;
387119
+ }
387120
+ };
387121
+
387122
+ // ../../packages/backend/src/runtimes/gybackend/startGyBackend.ts
386117
387123
  init_scheduledTaskRunner();
386118
387124
 
386119
387125
  // ../../packages/backend/src/services/history/HistoryStorageMigration.ts
@@ -393608,11 +394614,11 @@ function createObservability(deps) {
393608
394614
  const policyEngine = new AgtPolicyEngine({
393609
394615
  loadPolicy: async () => {
393610
394616
  const req = (0, import_node_module5.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta7.url);
393611
- const fs22 = req("node:fs");
394617
+ const fs23 = req("node:fs");
393612
394618
  const path32 = req("node:path");
393613
394619
  const policyPath = path32.join(".", "policy.yaml");
393614
- if (fs22.existsSync(policyPath)) {
393615
- return parsePolicyYaml(fs22.readFileSync(policyPath, "utf8"));
394620
+ if (fs23.existsSync(policyPath)) {
394621
+ return parsePolicyYaml(fs23.readFileSync(policyPath, "utf8"));
393616
394622
  }
393617
394623
  return {
393618
394624
  name: "rterm-default",
@@ -393654,18 +394660,18 @@ function createObservability(deps) {
393654
394660
  const scanRoots = [pluginScanRoot, "./plugins"];
393655
394661
  try {
393656
394662
  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);
394663
+ const fs23 = req("node:fs");
394664
+ if (fs23.existsSync(bundlePluginRoot)) scanRoots.push(bundlePluginRoot);
393659
394665
  } catch {
393660
394666
  }
393661
394667
  try {
393662
394668
  const req = (0, import_node_module5.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta7.url);
393663
- const fs22 = req("node:fs");
394669
+ const fs23 = req("node:fs");
393664
394670
  const path32 = req("node:path");
393665
394671
  const resourcesPath = process.resourcesPath;
393666
394672
  if (resourcesPath) {
393667
394673
  const resourcesPlugins = path32.join(resourcesPath, "plugins");
393668
- if (fs22.existsSync(resourcesPlugins)) scanRoots.push(resourcesPlugins);
394674
+ if (fs23.existsSync(resourcesPlugins)) scanRoots.push(resourcesPlugins);
393669
394675
  }
393670
394676
  } catch {
393671
394677
  }
@@ -396631,434 +397637,581 @@ async function startGyBackend() {
396631
397637
  gatewayService.broadcastRaw("skills:updated", result.skills);
396632
397638
  gatewayService.broadcastRaw("memory:updated", result.memory);
396633
397639
  };
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
397640
+ const gatewayRateLimiter = new GatewayRateLimiter({
397641
+ capacity: Number(import_node_process2.default.env.RTERM_RATE_LIMIT_BURST ?? 60) || 60,
397642
+ refillPerSecond: Number(import_node_process2.default.env.RTERM_RATE_LIMIT_REFILL ?? 1) || 1,
397643
+ authFailureLimit: 5,
397644
+ authLockoutMs: 6e4
397645
+ });
397646
+ const settingsDir = import_node_path28.default.dirname(settingsService.getSettingsPath?.() ?? "");
397647
+ const backupsDir = settingsDir ? import_node_path28.default.join(settingsDir, "backups") : "";
397648
+ const settingsBackupService = new SettingsBackupService(
397649
+ backupsDir ? {
397650
+ read: () => import_node_fs12.default.readFileSync(settingsService.getSettingsPath(), "utf8"),
397651
+ write: (c) => import_node_fs12.default.writeFileSync(settingsService.getSettingsPath(), c),
397652
+ listBackups: () => import_node_fs12.default.existsSync(backupsDir) ? import_node_fs12.default.readdirSync(backupsDir) : [],
397653
+ readBackup: (n2) => import_node_fs12.default.readFileSync(import_node_path28.default.join(backupsDir, n2), "utf8"),
397654
+ writeBackup: (n2, c) => {
397655
+ import_node_fs12.default.mkdirSync(backupsDir, { recursive: true });
397656
+ import_node_fs12.default.writeFileSync(import_node_path28.default.join(backupsDir, n2), c);
396641
397657
  },
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;
397658
+ deleteBackup: (n2) => {
397659
+ const p = import_node_path28.default.join(backupsDir, n2);
397660
+ if (import_node_fs12.default.existsSync(p)) import_node_fs12.default.unlinkSync(p);
397661
+ }
397662
+ } : { read: () => "", write: () => {
397663
+ }, listBackups: () => [], readBackup: () => "", writeBackup: () => {
397664
+ }, deleteBackup: () => {
397665
+ } },
397666
+ { keep: 20 }
397667
+ );
397668
+ const originalSetSettings = settingsService.setSettings.bind(settingsService);
397669
+ settingsService.setSettings = ((next) => {
397670
+ try {
397671
+ settingsBackupService.backup();
397672
+ } catch {
397673
+ }
397674
+ return originalSetSettings(next);
397675
+ });
397676
+ const idleTimeoutService = new IdleTimeoutService({
397677
+ idleMinutes: Number(import_node_process2.default.env.RTERM_IDLE_TIMEOUT_MINUTES ?? 30) || 30,
397678
+ protectedIds: [import_node_process2.default.env.GYBACKEND_TERMINAL_ID || "local-main"]
397679
+ });
397680
+ for (const t of terminalService.getAllTerminals()) idleTimeoutService.register(t.id);
397681
+ terminalService.onTerminalCreated?.((id) => idleTimeoutService.register(id));
397682
+ terminalService.onTerminalData?.((id) => idleTimeoutService.touch(id));
397683
+ terminalService.onTerminalClosed?.((id) => idleTimeoutService.forget(id));
397684
+ const idleSweeper = setInterval(() => {
397685
+ try {
397686
+ const idle = idleTimeoutService.idleTerminals(terminalService.getAllTerminals().map((t) => t.id));
397687
+ for (const { terminalId, idleMinutes } of idle) {
397688
+ console.log(`[gybackend] closing idle terminal ${terminalId} (${idleMinutes}min idle)`);
397689
+ try {
397690
+ terminalService.kill(terminalId);
397691
+ } catch {
397692
+ }
397693
+ idleTimeoutService.forget(terminalId);
397694
+ }
397695
+ } catch {
397696
+ }
397697
+ }, 6e4);
397698
+ if (typeof idleSweeper.unref === "function") idleSweeper.unref();
397699
+ let restDispatchTarget = null;
397700
+ const restDispatch = async (method, params) => {
397701
+ const target = restDispatchTarget;
397702
+ if (!target?.handleRequest) {
397703
+ throw new Error("REST API is not ready yet (gateway still starting)");
397704
+ }
397705
+ return await target.handleRequest(method, params);
397706
+ };
397707
+ const buildRestHttpRoutesSync = (opts) => {
397708
+ const routes = defaultRestRoutes();
397709
+ const staticRoutes = routes.filter((r) => !r.path.includes(":")).map((route) => ({
397710
+ path: route.path,
397711
+ handler: makeRestHandler(routes, opts)
397712
+ }));
397713
+ return staticRoutes;
397714
+ };
397715
+ const makeRestHandler = (routes, opts) => {
397716
+ return async (req, res) => {
397717
+ const r = req;
397718
+ const s = res;
397719
+ try {
397720
+ if (!await opts.isAuthorized(r)) {
397721
+ s.writeHead?.(401, { "content-type": "application/json" });
397722
+ s.end?.(JSON.stringify({ error: "unauthorized" }));
397723
+ return;
397724
+ }
397725
+ const url2 = new URL(r.url ?? "/", "http://localhost");
397726
+ const body = r.method === "POST" ? await readJsonBody(r) : Object.fromEntries(url2.searchParams.entries());
397727
+ const result = await handleRestRequest(routes, opts.dispatch, {
397728
+ method: r.method ?? "GET",
397729
+ path: url2.pathname,
397730
+ body
397731
+ });
397732
+ s.writeHead?.(result.status, { "content-type": "application/json" });
397733
+ s.end?.(JSON.stringify(result.body));
397734
+ } catch (e) {
397735
+ s.writeHead?.(500, { "content-type": "application/json" });
397736
+ s.end?.(JSON.stringify({ error: "internal", message: e instanceof Error ? e.message : String(e) }));
397737
+ }
397738
+ };
397739
+ };
397740
+ const readJsonBody = (req) => new Promise((resolve2) => {
397741
+ let data = "";
397742
+ req.on?.("data", (d) => {
397743
+ data += String(d ?? "");
397744
+ });
397745
+ req.on?.("end", () => {
397746
+ try {
397747
+ resolve2(data ? JSON.parse(data) : {});
397748
+ } catch {
397749
+ resolve2({});
397750
+ }
397751
+ });
397752
+ });
397753
+ const wsGatewayControlService = new WebSocketGatewayControlService({
397754
+ createAdapter: (host, port, ipFilter) => {
397755
+ const adapter = new WebSocketGatewayAdapter(gatewayService, {
397756
+ host,
397757
+ port,
397758
+ accessTokenAuth: {
397759
+ verifyToken: (token) => accessTokenService.verifyToken(token),
397760
+ allowLocalhostWithoutToken: true
397761
+ },
397762
+ // v3.2.18: per-client rate limiting (token bucket + auth lockout).
397763
+ rateLimiter: gatewayRateLimiter,
397764
+ ipFilter,
397765
+ // Browser dashboard on the SAME port as the WS gateway: /dashboard serves
397766
+ // a live page (WS push via observability:liveDashboardSubscribe, with a
397767
+ // /dashboard/json polling fallback). Auth mirrors the WS gateway —
397768
+ // loopback open, remote needs a valid access token.
397769
+ // v3.2.18: REST API routes (/api/v1/*) are prepended so curl/CI can
397770
+ // drive the gateway without a WS client.
397771
+ httpRoutes: [
397772
+ ...buildRestHttpRoutesSync({
397773
+ isAuthorized: (req) => dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t)),
397774
+ dispatch: restDispatch
397775
+ }),
397776
+ {
397777
+ path: "/dashboard",
397778
+ handler: async (req, res) => {
397779
+ if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
397780
+ res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
397781
+ res.end("missing/invalid access token");
397782
+ return;
397783
+ }
397784
+ const state = await observability.dashboard.state();
397785
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
397786
+ res.end(
397787
+ renderLiveDashboardHtml(state, {
397788
+ title: "RTerm \xB7 Unified Dashboard",
397789
+ dataUrl: "/dashboard/json"
397790
+ })
397791
+ );
397792
+ }
397793
+ },
397794
+ {
397795
+ path: "/dashboard/json",
397796
+ handler: async (req, res) => {
397797
+ if (!await dashboardHttpAuthorized(req, (t) => accessTokenService.verifyToken(t))) {
397798
+ res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
397799
+ res.end("missing/invalid access token");
397800
+ return;
397801
+ }
397802
+ const state = await observability.dashboard.state();
397803
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
397804
+ res.end(JSON.stringify(state));
396655
397805
  }
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
397806
  }
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;
397807
+ ],
397808
+ terminalBridge: {
397809
+ listTerminals: () => terminalService.getDisplayTerminals().map((terminal) => ({
397810
+ id: terminal.id,
397811
+ title: terminal.title,
397812
+ type: terminal.type,
397813
+ cols: terminal.cols,
397814
+ rows: terminal.rows,
397815
+ runtimeState: terminal.runtimeState,
397816
+ lastExitCode: terminal.lastExitCode,
397817
+ monitorIdentity: terminalService.getMonitorIdentity(terminal.id) ?? void 0
397818
+ })),
397819
+ createTab: async (config2) => {
397820
+ const snapshot = terminalService.getDisplayTerminals();
397821
+ const normalized = createAutoTerminalConfig(snapshot, config2);
397822
+ const tab = await terminalService.createTerminal(normalized);
397823
+ return { id: tab.id };
397824
+ },
397825
+ write: async (terminalId, data) => {
397826
+ terminalService.write(terminalId, data);
397827
+ },
397828
+ writePaths: async (terminalId, paths) => {
397829
+ terminalService.writePaths(terminalId, paths);
397830
+ },
397831
+ resize: async (terminalId, cols, rows) => {
397832
+ terminalService.resize(terminalId, cols, rows);
397833
+ },
397834
+ kill: async (terminalId) => {
397835
+ if (terminalService.getDisplayTerminals().length <= 1) {
397836
+ throw new Error("Cannot close the last terminal tab.");
396673
397837
  }
396674
- const state = await observability.dashboard.state();
396675
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
396676
- res.end(JSON.stringify(state));
397838
+ terminalService.kill(terminalId);
397839
+ },
397840
+ reconnect: async (terminalId) => {
397841
+ const tab = await terminalService.reconnectTerminal(terminalId);
397842
+ return { id: tab.id };
397843
+ },
397844
+ setTitle: async (terminalId, title) => {
397845
+ terminalService.setTitle(terminalId, title);
397846
+ },
397847
+ setSelection: async (terminalId, selectionText) => {
397848
+ terminalService.setSelection(terminalId, selectionText);
397849
+ },
397850
+ getBufferDelta: async (terminalId, fromOffset) => {
397851
+ const data = terminalService.getBufferDelta(terminalId, fromOffset);
397852
+ const offset = terminalService.getCurrentOffset(terminalId);
397853
+ return { data, offset };
397854
+ },
397855
+ generateCommandDraft: async (terminalId, prompt, profileId) => {
397856
+ return await terminalCommandDraftService.generateCommandDraft({
397857
+ terminalId,
397858
+ prompt,
397859
+ profileId
397860
+ });
396677
397861
  }
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
397862
  },
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.");
397863
+ filesystemBridge: {
397864
+ listDirectory: async (terminalId, dirPath) => {
397865
+ return await fileSystemService.listDirectory(terminalId, dirPath);
397866
+ },
397867
+ readTextFile: async (terminalId, filePath, options) => {
397868
+ return await fileSystemService.readTextFile(
397869
+ terminalId,
397870
+ filePath,
397871
+ options
397872
+ );
397873
+ },
397874
+ readFileBase64: async (terminalId, filePath, options) => {
397875
+ return await fileSystemService.readFileBase64(
397876
+ terminalId,
397877
+ filePath,
397878
+ options
397879
+ );
397880
+ },
397881
+ writeTextFile: async (terminalId, filePath, content) => {
397882
+ await fileSystemService.writeTextFile(
397883
+ terminalId,
397884
+ filePath,
397885
+ content
397886
+ );
397887
+ },
397888
+ writeFileBase64: async (terminalId, filePath, contentBase64, options) => {
397889
+ await fileSystemService.writeFileBase64(
397890
+ terminalId,
397891
+ filePath,
397892
+ contentBase64,
397893
+ options
397894
+ );
397895
+ },
397896
+ transferEntries: async (sourceTerminalId, sourcePaths, targetTerminalId, targetDirPath, options) => {
397897
+ return await fileSystemService.transferEntries(
397898
+ sourceTerminalId,
397899
+ sourcePaths,
397900
+ targetTerminalId,
397901
+ targetDirPath,
397902
+ options
397903
+ );
397904
+ },
397905
+ startTransfer: async (input) => {
397906
+ return fileTransferService.startTransfer(input);
397907
+ },
397908
+ getTransfer: async (transferId) => {
397909
+ return fileTransferService.getTransfer(transferId);
397910
+ },
397911
+ listTransfers: async (options) => {
397912
+ return fileTransferService.listTransfers(options);
397913
+ },
397914
+ cancelTransfer: async (transferId) => {
397915
+ return fileTransferService.cancelTransfer(transferId);
397916
+ },
397917
+ cancelTransferTask: async (transferId) => {
397918
+ return fileTransferService.cancelTransfer(transferId);
397919
+ },
397920
+ createDirectory: async (terminalId, dirPath) => {
397921
+ await fileSystemService.createDirectory(terminalId, dirPath);
397922
+ },
397923
+ createFile: async (terminalId, filePath) => {
397924
+ await fileSystemService.createFile(terminalId, filePath);
397925
+ },
397926
+ deletePath: async (terminalId, targetPath, options) => {
397927
+ await fileSystemService.deletePath(terminalId, targetPath, options);
397928
+ },
397929
+ renamePath: async (terminalId, sourcePath, targetPath) => {
397930
+ await fileSystemService.renamePath(
397931
+ terminalId,
397932
+ sourcePath,
397933
+ targetPath
397934
+ );
396709
397935
  }
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
397936
  },
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
397937
+ profileBridge: {
397938
+ getProfiles: () => {
397939
+ const snapshot = settingsService.getSettings();
397940
+ const modelNameById = new Map(
397941
+ snapshot.models.items.map((item) => [item.id, item.model])
397942
+ );
397943
+ return {
397944
+ activeProfileId: snapshot.models.activeProfileId,
397945
+ profiles: snapshot.models.profiles.map((profile) => ({
397946
+ id: profile.id,
397947
+ name: profile.name,
397948
+ globalModelId: profile.globalModelId,
397949
+ modelName: modelNameById.get(profile.globalModelId)
397950
+ }))
397951
+ };
397952
+ },
397953
+ setActiveProfile: (profileId) => {
397954
+ const snapshot = settingsService.getSettings();
397955
+ const exists = snapshot.models.profiles.some(
397956
+ (profile) => profile.id === profileId
397957
+ );
397958
+ if (!exists) {
397959
+ throw new Error(`Profile not found: ${profileId}`);
396838
397960
  }
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
397961
+ settingsService.setSettings({
397962
+ models: {
397963
+ items: snapshot.models.items,
397964
+ profiles: snapshot.models.profiles,
397965
+ activeProfileId: profileId
397966
+ }
397967
+ });
397968
+ const next = settingsService.getSettings();
397969
+ agentService.updateSettings(next);
397970
+ const modelNameById = new Map(
397971
+ next.models.items.map((item) => [item.id, item.model])
396871
397972
  );
396872
397973
  return {
396873
- sessionId,
396874
- mode,
396875
- title: uiSession?.title || backendSession.title,
396876
- content: markdown
397974
+ activeProfileId: next.models.activeProfileId,
397975
+ profiles: next.models.profiles.map((profile) => ({
397976
+ id: profile.id,
397977
+ name: profile.name,
397978
+ globalModelId: profile.globalModelId,
397979
+ modelName: modelNameById.get(profile.globalModelId)
397980
+ }))
396877
397981
  };
397982
+ },
397983
+ probeModel: async (model) => {
397984
+ return await modelCapabilityService.probe(model);
396878
397985
  }
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
397986
  },
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
397987
+ agentBridge: {
397988
+ exportHistory: async (sessionId, mode) => {
397989
+ await gatewayService.waitForRunCompletion(sessionId);
397990
+ const backendSession = agentService.exportChatSession(sessionId);
397991
+ if (!backendSession) {
397992
+ throw new Error(`Session with ID ${sessionId} not found`);
397993
+ }
397994
+ const uiSession = uiHistoryService.getSession(sessionId);
397995
+ if (mode === "simple") {
397996
+ const markdown = uiHistoryService.toReadableMarkdown(
397997
+ uiSession?.messages || [],
397998
+ uiSession?.title || backendSession.title
397999
+ );
398000
+ return {
398001
+ sessionId,
398002
+ mode,
398003
+ title: uiSession?.title || backendSession.title,
398004
+ content: markdown
398005
+ };
396938
398006
  }
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;
398007
+ return {
398008
+ sessionId: backendSession.id,
398009
+ mode,
398010
+ title: uiSession?.title || backendSession.title,
398011
+ lastCheckpointOffset: backendSession.lastCheckpointOffset,
398012
+ createdAt: new Date(backendSession.createdAt).toISOString(),
398013
+ updatedAt: new Date(backendSession.updatedAt).toISOString(),
398014
+ frontendMessages: uiSession?.messages || [],
398015
+ backendMessages: backendSession.messages.map((msg) => ({
398016
+ messageId: msg.id,
398017
+ messageType: msg.type,
398018
+ messageData: msg.data
398019
+ }))
398020
+ };
398021
+ },
398022
+ getAllChatHistory: () => agentService.getAllChatHistory(),
398023
+ loadChatSession: (sessionId) => agentService.loadChatSession(sessionId),
398024
+ getUiMessages: (sessionId) => uiHistoryService.getMessages(sessionId)
396983
398025
  },
396984
- apply: async (profileId) => {
396985
- const result = await agentSettingProfileService.apply(profileId);
396986
- broadcastAgentSettingResult(result);
396987
- return result;
398026
+ systemBridge: {
398027
+ saveImageAttachment: async (payload) => {
398028
+ return await imageAttachmentService.saveImageAttachment(payload);
398029
+ }
396988
398030
  },
396989
- overwrite: async (profileId) => {
396990
- const result = await agentSettingProfileService.overwrite(profileId);
396991
- broadcastAgentSettingResult(result);
396992
- return result;
398031
+ skillBridge: {
398032
+ reload: async () => {
398033
+ return await skillService.reload();
398034
+ },
398035
+ getAll: async () => {
398036
+ return await skillService.getAll();
398037
+ },
398038
+ getEnabled: async () => {
398039
+ return await skillService.getEnabledSkills();
398040
+ },
398041
+ create: async () => {
398042
+ return await skillService.createSkillFromTemplate();
398043
+ },
398044
+ delete: async (fileName) => {
398045
+ await skillService.deleteSkillFile(fileName);
398046
+ return await skillService.getAll();
398047
+ },
398048
+ listSkills: async () => {
398049
+ const snapshot = settingsService.getSettings();
398050
+ const enabledMap = snapshot.tools?.skills ?? {};
398051
+ const skills = await skillService.getAll();
398052
+ return skills.map((skill) => ({
398053
+ name: skill.name,
398054
+ description: skill.description,
398055
+ enabled: enabledMap[skill.name] !== false
398056
+ }));
398057
+ },
398058
+ setSkillEnabled: async (name, enabled) => {
398059
+ const snapshot = settingsService.getSettings();
398060
+ const nextSkills = { ...snapshot.tools?.skills ?? {} };
398061
+ nextSkills[name] = enabled;
398062
+ settingsService.setSettings({
398063
+ tools: {
398064
+ builtIn: snapshot.tools?.builtIn ?? {},
398065
+ skills: nextSkills
398066
+ }
398067
+ });
398068
+ const next = settingsService.getSettings();
398069
+ agentService.updateSettings(next);
398070
+ const skills = await skillService.getAll();
398071
+ const summary = buildSkillStatusSummary(skills, next.tools?.skills);
398072
+ gatewayService.broadcastRaw("skills:updated", summary);
398073
+ return summary;
398074
+ }
396993
398075
  },
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."
398076
+ memoryBridge: {
398077
+ get: async () => {
398078
+ return await memoryService.getMemorySnapshot(
398079
+ settingsService.getSettings().agentSettings?.activeProfileId || null
397006
398080
  );
398081
+ },
398082
+ setContent: async (content) => {
398083
+ const snapshot = await memoryService.writeMemory(
398084
+ content,
398085
+ settingsService.getSettings().agentSettings?.activeProfileId || null
398086
+ );
398087
+ gatewayService.broadcastRaw("memory:updated", snapshot);
398088
+ return snapshot;
398089
+ },
398090
+ search: async (query, limit2) => {
398091
+ const { content } = await memoryService.getMemorySnapshot(
398092
+ settingsService.getSettings().agentSettings?.activeProfileId || null
398093
+ );
398094
+ return searchMemory(content, query, limit2 ?? 10);
398095
+ },
398096
+ append: async (note) => {
398097
+ const profileId = settingsService.getSettings().agentSettings?.activeProfileId || null;
398098
+ const { content } = await memoryService.getMemorySnapshot(profileId);
398099
+ const next = appendMemoryNote(content, note);
398100
+ const snapshot = await memoryService.writeMemory(next, profileId);
398101
+ gatewayService.broadcastRaw("memory:updated", snapshot);
398102
+ return snapshot;
397007
398103
  }
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
398104
  },
397018
- addRule: async (listName, rule) => {
397019
- return await commandPolicyService.addRule(listName, rule);
398105
+ agentSettingsBridge: {
398106
+ get: () => agentSettingProfileService.getState(),
398107
+ saveCurrent: async () => {
398108
+ const result = await agentSettingProfileService.saveCurrent();
398109
+ broadcastAgentSettingResult(result);
398110
+ return result;
398111
+ },
398112
+ apply: async (profileId) => {
398113
+ const result = await agentSettingProfileService.apply(profileId);
398114
+ broadcastAgentSettingResult(result);
398115
+ return result;
398116
+ },
398117
+ overwrite: async (profileId) => {
398118
+ const result = await agentSettingProfileService.overwrite(profileId);
398119
+ broadcastAgentSettingResult(result);
398120
+ return result;
398121
+ },
398122
+ delete: async (profileId) => {
398123
+ const result = await agentSettingProfileService.delete(profileId);
398124
+ broadcastAgentSettingResult(result);
398125
+ return result;
398126
+ }
397020
398127
  },
397021
- deleteRule: async (listName, rule) => {
397022
- return await commandPolicyService.deleteRule(listName, rule);
397023
- }
397024
- },
397025
- toolsBridge: {
397026
- reloadMcp: async () => {
397027
- return await mcpToolService.reloadAll();
398128
+ settingsBridge: {
398129
+ getSettings: () => settingsService.getSettings(),
398130
+ setSettings: async (patch) => {
398131
+ if (patch?.gateway?.ws) {
398132
+ throw new Error(
398133
+ "settings.gateway.ws is not configurable via websocket RPC."
398134
+ );
398135
+ }
398136
+ settingsService.setSettings(patch);
398137
+ const next = settingsService.getSettings();
398138
+ agentService.updateSettings(next);
398139
+ return next;
398140
+ },
398141
+ // v3.2.18: settings backup/restore/export/import.
398142
+ listBackups: () => settingsBackupService.list(),
398143
+ restoreBackup: async (name) => settingsBackupService.restore(name),
398144
+ export: () => settingsBackupService.export(),
398145
+ import: async (content) => settingsBackupService.import(content)
397028
398146
  },
397029
- getMcp: () => mcpToolService.getSummaries(),
397030
- setMcpEnabled: async (name, enabled) => {
397031
- return await mcpToolService.setServerEnabled(name, enabled);
398147
+ // v3.2.18: cross-session history search.
398148
+ historyBridge: {
398149
+ getAllSessions: async () => {
398150
+ try {
398151
+ const all = historyStore.listChatSessions();
398152
+ if (Array.isArray(all) && all.length > 0) {
398153
+ return all;
398154
+ }
398155
+ } catch (e) {
398156
+ console.warn(
398157
+ "[history:search] store load failed, falling back to metadata:",
398158
+ e instanceof Error ? e.message : e
398159
+ );
398160
+ }
398161
+ return agentService.getAllChatHistory() ?? [];
398162
+ }
397032
398163
  },
397033
- getBuiltIn: () => {
397034
- const settings = settingsService.getSettings();
397035
- return buildBuiltInToolStatusSummary(settings.tools?.builtIn);
398164
+ commandPolicyBridge: {
398165
+ getLists: async () => {
398166
+ return await commandPolicyService.getLists();
398167
+ },
398168
+ addRule: async (listName, rule) => {
398169
+ return await commandPolicyService.addRule(listName, rule);
398170
+ },
398171
+ deleteRule: async (listName, rule) => {
398172
+ return await commandPolicyService.deleteRule(listName, rule);
398173
+ }
397036
398174
  },
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
- })
398175
+ toolsBridge: {
398176
+ reloadMcp: async () => {
398177
+ return await mcpToolService.reloadAll();
398178
+ },
398179
+ getMcp: () => mcpToolService.getSummaries(),
398180
+ setMcpEnabled: async (name, enabled) => {
398181
+ return await mcpToolService.setServerEnabled(name, enabled);
398182
+ },
398183
+ getBuiltIn: () => {
398184
+ const settings = settingsService.getSettings();
398185
+ return buildBuiltInToolStatusSummary(settings.tools?.builtIn);
398186
+ },
398187
+ setBuiltInEnabled: async (name, enabled) => {
398188
+ const settings = settingsService.getSettings();
398189
+ const nextBuiltIn = { ...settings.tools?.builtIn ?? {} };
398190
+ nextBuiltIn[name] = enabled;
398191
+ settingsService.setSettings({
398192
+ tools: {
398193
+ builtIn: nextBuiltIn,
398194
+ skills: settings.tools?.skills ?? {}
398195
+ }
398196
+ });
398197
+ const next = settingsService.getSettings();
398198
+ agentService.updateSettings(next);
398199
+ const summary = buildBuiltInToolStatusSummary(next.tools?.builtIn);
398200
+ gatewayService.broadcastRaw("tools:builtInUpdated", summary);
398201
+ return summary;
398202
+ }
398203
+ },
398204
+ // Observability bridge (v2.9.x): exposes the 9 platform capabilities
398205
+ // (metrics export, secrets, on-call, cost, recording, gitops, playbooks,
398206
+ // cloud, live dashboard) as observability:* RPC methods.
398207
+ observabilityBridge: createObservabilityBridge({
398208
+ observability: () => observability,
398209
+ terminalService: () => terminalService
398210
+ })
398211
+ });
398212
+ restDispatchTarget = adapter;
398213
+ return adapter;
398214
+ }
397062
398215
  });
397063
398216
  await wsGatewayControlService.applyPolicy(startupPolicy);
397064
398217
  const shutdown = async (signal) => {