neuralos 3.3.9 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/gybackend.cjs CHANGED
@@ -258528,6 +258528,81 @@ var init_HistorySqliteStore = __esm({
258528
258528
  });
258529
258529
  })();
258530
258530
  }
258531
+ /**
258532
+ * v3.4.1: append only the NEW messages for a session instead of rewriting
258533
+ * the whole session. saveUiSessions() deletes every row and re-inserts the
258534
+ * entire message list — on a long session that is a large synchronous
258535
+ * better-sqlite3 transaction on the main event loop, which is the
258536
+ * spinning-wheel freeze. This method appends from a given position in one
258537
+ * small transaction, so a debounced flush costs O(new messages), not
258538
+ * O(all messages).
258539
+ *
258540
+ * Returns the number of rows appended.
258541
+ */
258542
+ appendUiSessionMessages(sessionId, messages, fromPosition, summary) {
258543
+ if (messages.length === 0 || fromPosition >= messages.length) {
258544
+ return 0;
258545
+ }
258546
+ const upsertSession = this.db.prepare(
258547
+ `INSERT INTO ui_sessions (
258548
+ id, title, updated_at, messages_count, last_message_preview
258549
+ ) VALUES (
258550
+ @id, @title, @updatedAt, @messagesCount, @lastMessagePreview
258551
+ )
258552
+ ON CONFLICT(id) DO UPDATE SET
258553
+ title = excluded.title,
258554
+ updated_at = excluded.updated_at,
258555
+ messages_count = excluded.messages_count,
258556
+ last_message_preview = excluded.last_message_preview`
258557
+ );
258558
+ const insertMessage = this.db.prepare(
258559
+ `INSERT INTO ui_session_messages (
258560
+ session_id, position, ui_message_id, backend_message_id, role, message_type, content, metadata_json, timestamp, streaming
258561
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
258562
+ );
258563
+ const slice = messages.slice(fromPosition);
258564
+ this.db.transaction(() => {
258565
+ if (summary) {
258566
+ upsertSession.run({
258567
+ id: sessionId,
258568
+ title: summary.title,
258569
+ updatedAt: summary.updatedAt,
258570
+ messagesCount: summary.messagesCount,
258571
+ lastMessagePreview: summary.lastMessagePreview
258572
+ });
258573
+ } else {
258574
+ const existing = this.db.prepare("SELECT title, updated_at FROM ui_sessions WHERE id = ?").get(sessionId);
258575
+ upsertSession.run({
258576
+ id: sessionId,
258577
+ title: existing?.title ?? "New Chat",
258578
+ updatedAt: Date.now(),
258579
+ messagesCount: (existing ? 0 : 0) + fromPosition + slice.length,
258580
+ lastMessagePreview: slice[slice.length - 1]?.content?.slice(0, 200) ?? ""
258581
+ });
258582
+ }
258583
+ for (let i = 0; i < slice.length; i++) {
258584
+ const message = slice[i];
258585
+ insertMessage.run(
258586
+ sessionId,
258587
+ fromPosition + i,
258588
+ message.id,
258589
+ message.backendMessageId ?? null,
258590
+ message.role,
258591
+ message.type,
258592
+ message.content,
258593
+ message.metadata ? JSON.stringify(message.metadata) : null,
258594
+ message.timestamp,
258595
+ message.streaming ? 1 : 0
258596
+ );
258597
+ }
258598
+ })();
258599
+ return slice.length;
258600
+ }
258601
+ /** v3.4.1: how many messages are already persisted for a session. */
258602
+ countUiSessionMessages(sessionId) {
258603
+ const row = this.db.prepare("SELECT COUNT(*) AS n FROM ui_session_messages WHERE session_id = ?").get(sessionId);
258604
+ return row?.n ?? 0;
258605
+ }
258531
258606
  deleteUiSessions(sessionIds) {
258532
258607
  const ids = Array.from(
258533
258608
  new Set(sessionIds.filter((id) => id.trim().length > 0))
@@ -350712,12 +350787,12 @@ var BUILTIN_TOOL_INFO = [
350712
350787
  {
350713
350788
  name: "write_file",
350714
350789
  description: WRITE_FILE_TOOL_DESCRIPTION,
350715
- hiddenFromSettings: true
350790
+ shortDescription: "Write a full file (replace contents)"
350716
350791
  },
350717
350792
  {
350718
350793
  name: "edit_file",
350719
350794
  description: EDIT_FILE_TOOL_DESCRIPTION,
350720
- hiddenFromSettings: true
350795
+ shortDescription: "Edit a file by replacing an exact string"
350721
350796
  },
350722
350797
  {
350723
350798
  name: "skill",
@@ -354867,6 +354942,7 @@ var CORE_METHODS = [
354867
354942
  m("tools:getMcp", "tools", "List MCP tools.", "1.0.0"),
354868
354943
  m("tools:setMcpEnabled", "tools", "Enable/disable an MCP tool.", "1.0.0"),
354869
354944
  m("tools:getBuiltIn", "tools", "List built-in agent tools with enabled state.", "1.0.0"),
354945
+ m("tools:getPlugins", "tools", "List plugin agent tools (name, plugin, description).", "3.4.0"),
354870
354946
  m("tools:setBuiltInEnabled", "tools", "Enable/disable a built-in tool.", "1.0.0", { name: { type: "string" }, enabled: { type: "boolean" } })
354871
354947
  ];
354872
354948
  var DESCRIBE_METHOD = m(
@@ -369458,6 +369534,7 @@ var AgentService_v2 = class {
369458
369534
  pluginTools = /* @__PURE__ */ new Map();
369459
369535
  /** Plugin tool schemas (for toolsForModel injection). */
369460
369536
  pluginToolSchemas = [];
369537
+ pluginToolMeta = [];
369461
369538
  passChatTempExportService = new PassChatTempExportService();
369462
369539
  fallbackCompactionHistoryExportService = null;
369463
369540
  activeAgentRunIdsBySession = /* @__PURE__ */ new Map();
@@ -369516,11 +369593,35 @@ var AgentService_v2 = class {
369516
369593
  * pluginTools (so the dispatch switch's default case can call them). */
369517
369594
  setPluginTools(tools2) {
369518
369595
  this.pluginTools = new Map(tools2.map((t) => [t.name, t.handler]));
369519
- this.pluginToolSchemas = tools2.map((t) => ({
369596
+ this.pluginToolMeta = tools2.map((t) => ({
369520
369597
  name: t.name,
369521
- description: t.description,
369522
- schema: t.params || {}
369598
+ description: t.description || t.name,
369599
+ plugin: t.plugin || "plugin"
369523
369600
  }));
369601
+ this.pluginToolSchemas = tools2.map((t) => {
369602
+ const params = t.params && typeof t.params === "object" ? t.params : {};
369603
+ const looksJsonSchema = typeof params.type === "string";
369604
+ const parameters = looksJsonSchema ? params : {
369605
+ type: "object",
369606
+ properties: params,
369607
+ additionalProperties: true
369608
+ };
369609
+ return {
369610
+ type: "function",
369611
+ function: {
369612
+ name: t.name,
369613
+ description: t.description || t.name,
369614
+ parameters
369615
+ }
369616
+ };
369617
+ });
369618
+ }
369619
+ /** Plugin tools in OpenAI bindTools shape (empty until setPluginTools). */
369620
+ getPluginToolSchemas() {
369621
+ return this.pluginToolSchemas;
369622
+ }
369623
+ listPluginTools() {
369624
+ return this.pluginToolMeta.map((t) => ({ ...t, enabled: true }));
369524
369625
  }
369525
369626
  /** Wire a session-log handle so list_session_logs / read_session_log work. */
369526
369627
  setSessionLogger(logger) {
@@ -370142,9 +370243,11 @@ var AgentService_v2 = class {
370142
370243
  }
370143
370244
  );
370144
370245
  const baseModel = shouldUseThinkingModelOnThisPass ? sessionBinding.thinkingModel || sessionBinding.model : sessionBinding.model;
370246
+ const pluginOpenAiTools = this.pluginToolSchemas;
370145
370247
  const modelWithTools = baseModel.bindTools([
370146
370248
  ...builtInTools,
370147
- ...mcpTools
370249
+ ...mcpTools,
370250
+ ...pluginOpenAiTools
370148
370251
  ]);
370149
370252
  const messageId = v4_default();
370150
370253
  let partialText = "";
@@ -370180,7 +370283,11 @@ var AgentService_v2 = class {
370180
370283
  shouldUseThinkingModelOnThisPass ? 0.2 : 0.7,
370181
370284
  null
370182
370285
  );
370183
- modelToUse = fallbackChat.bindTools([...builtInTools, ...mcpTools]);
370286
+ modelToUse = fallbackChat.bindTools([
370287
+ ...builtInTools,
370288
+ ...mcpTools,
370289
+ ...pluginOpenAiTools
370290
+ ]);
370184
370291
  }
370185
370292
  return await invokeWithRetryAndSanitizedInput({
370186
370293
  helpers: this.helpers,
@@ -373372,16 +373479,84 @@ function buildUiSessionSummary(session) {
373372
373479
  }
373373
373480
 
373374
373481
  // ../../packages/backend/src/services/UIHistoryService.ts
373375
- var UIHistoryService = class {
373482
+ var UIHistoryService = class _UIHistoryService {
373376
373483
  store;
373377
373484
  sessionsCache = {};
373378
373485
  sessionSummaryCache = {};
373379
373486
  dirtySessions = /* @__PURE__ */ new Set();
373487
+ /**
373488
+ * v3.4.1: debounced auto-flush. Previously recordEvent() marked a session
373489
+ * dirty but NEVER flushed — messages reached SQLite only on rename /
373490
+ * rollback / branch or a graceful app close. Kill the process mid-run
373491
+ * (freeze, crash, force-quit) and every message since the last flush was
373492
+ * lost. That is the "work done, no record" bug.
373493
+ *
373494
+ * Now: flush within FLUSH_DEBOUNCE_MS of the last event, plus a synchronous
373495
+ * flush on beforeExit / SIGINT / SIGTERM.
373496
+ */
373497
+ flushTimer = null;
373498
+ shutdownHooksInstalled = false;
373499
+ /** v3.4.1: messages already written to SQLite per session, so flush()
373500
+ * appends only the new ones instead of rewriting the whole session. */
373501
+ persistedMessageCount = /* @__PURE__ */ new Map();
373502
+ /** Flush debounce window — short enough to survive a crash, long enough
373503
+ * to batch a streaming burst into one write. */
373504
+ static FLUSH_DEBOUNCE_MS = 1500;
373380
373505
  constructor(options) {
373381
373506
  this.store = options?.store || new HistorySqliteStore();
373382
373507
  this.sessionSummaryCache = this.buildSessionSummaryCache(
373383
373508
  this.store.listUiSessionSummaries()
373384
373509
  );
373510
+ this.installShutdownHooks();
373511
+ }
373512
+ installShutdownHooks() {
373513
+ if (this.shutdownHooksInstalled) return;
373514
+ this.shutdownHooksInstalled = true;
373515
+ const flushNow = () => {
373516
+ try {
373517
+ this.flush();
373518
+ } catch {
373519
+ }
373520
+ };
373521
+ process.once("beforeExit", flushNow);
373522
+ for (const sig of ["SIGINT", "SIGTERM"]) {
373523
+ if (!_UIHistoryService.activeFlushOnSignal.has(sig)) {
373524
+ _UIHistoryService.activeFlushOnSignal.add(sig);
373525
+ const hadListeners = process.listenerCount(sig) > 0;
373526
+ process.once(sig, () => {
373527
+ for (const svc of _UIHistoryService.instances) {
373528
+ try {
373529
+ svc.flush();
373530
+ } catch {
373531
+ }
373532
+ }
373533
+ _UIHistoryService.activeFlushOnSignal.delete(sig);
373534
+ if (!hadListeners) {
373535
+ process.kill(process.pid, sig);
373536
+ }
373537
+ });
373538
+ }
373539
+ }
373540
+ _UIHistoryService.instances.add(this);
373541
+ }
373542
+ /** Live instances — the shared signal handler flushes all of them. */
373543
+ static instances = /* @__PURE__ */ new Set();
373544
+ static activeFlushOnSignal = /* @__PURE__ */ new Set();
373545
+ scheduleFlush() {
373546
+ if (this.flushTimer) {
373547
+ clearTimeout(this.flushTimer);
373548
+ }
373549
+ this.flushTimer = setTimeout(() => {
373550
+ this.flushTimer = null;
373551
+ try {
373552
+ this.flush();
373553
+ } catch (error40) {
373554
+ console.error("[UIHistory] auto-flush failed:", error40);
373555
+ }
373556
+ }, _UIHistoryService.FLUSH_DEBOUNCE_MS);
373557
+ if (typeof this.flushTimer.unref === "function") {
373558
+ this.flushTimer.unref();
373559
+ }
373385
373560
  }
373386
373561
  buildSessionSummaryCache(summaries) {
373387
373562
  const cache2 = {};
@@ -373401,6 +373576,9 @@ var UIHistoryService = class {
373401
373576
  }
373402
373577
  const sanitized = sanitizeUiSession(loaded);
373403
373578
  this.sessionsCache[sessionId] = sanitized;
373579
+ if (!this.persistedMessageCount.has(sessionId)) {
373580
+ this.persistedMessageCount.set(sessionId, sanitized.messages.length);
373581
+ }
373404
373582
  this.syncSessionSummary(sessionId);
373405
373583
  return sanitized;
373406
373584
  }
@@ -373427,6 +373605,7 @@ var UIHistoryService = class {
373427
373605
  const actions = this.processEvent(session, event, sessionId);
373428
373606
  this.syncSessionSummary(sessionId);
373429
373607
  this.dirtySessions.add(sessionId);
373608
+ this.scheduleFlush();
373430
373609
  return actions;
373431
373610
  }
373432
373611
  flush(sessionId) {
@@ -373435,6 +373614,7 @@ var UIHistoryService = class {
373435
373614
  return;
373436
373615
  }
373437
373616
  const entries = [];
373617
+ const appendFrom = {};
373438
373618
  sessionIds.forEach((id) => {
373439
373619
  const session = this.sessionsCache[id];
373440
373620
  if (!session) {
@@ -373446,9 +373626,34 @@ var UIHistoryService = class {
373446
373626
  const summary = buildUiSessionSummary(sanitized);
373447
373627
  this.sessionSummaryCache[id] = summary;
373448
373628
  entries.push({ session: sanitized, summary });
373629
+ const already = this.persistedMessageCount.get(id);
373630
+ appendFrom[id] = typeof already === "number" && already <= sanitized.messages.length ? already : 0;
373449
373631
  });
373450
373632
  if (entries.length > 0) {
373451
- this.store.saveUiSessions(entries);
373633
+ let usedIncremental = false;
373634
+ try {
373635
+ let appended = 0;
373636
+ for (const { session, summary } of entries) {
373637
+ const from = appendFrom[session.id] ?? 0;
373638
+ const n2 = this.store.appendUiSessionMessages(
373639
+ session.id,
373640
+ session.messages,
373641
+ from,
373642
+ summary
373643
+ );
373644
+ appended += n2;
373645
+ this.persistedMessageCount.set(session.id, session.messages.length);
373646
+ }
373647
+ usedIncremental = true;
373648
+ } catch {
373649
+ usedIncremental = false;
373650
+ }
373651
+ if (!usedIncremental) {
373652
+ this.store.saveUiSessions(entries);
373653
+ entries.forEach(
373654
+ ({ session }) => this.persistedMessageCount.set(session.id, session.messages.length)
373655
+ );
373656
+ }
373452
373657
  }
373453
373658
  sessionIds.forEach((id) => this.dirtySessions.delete(id));
373454
373659
  }
@@ -373961,6 +374166,7 @@ Error: ${event.message}` : ""),
373961
374166
  delete this.sessionsCache[id];
373962
374167
  delete this.sessionSummaryCache[id];
373963
374168
  this.dirtySessions.delete(id);
374169
+ this.persistedMessageCount.delete(id);
373964
374170
  });
373965
374171
  this.store.deleteUiSessions(ids);
373966
374172
  }
@@ -373999,6 +374205,7 @@ Error: ${event.message}` : ""),
373999
374205
  );
374000
374206
  this.syncSessionSummary(sessionId);
374001
374207
  this.dirtySessions.add(sessionId);
374208
+ this.persistedMessageCount.delete(sessionId);
374002
374209
  this.flush(sessionId);
374003
374210
  return removedCount;
374004
374211
  }
@@ -376649,6 +376856,15 @@ var WebSocketGatewayAdapter = class {
376649
376856
  }
376650
376857
  return await this.options.toolsBridge.getBuiltIn();
376651
376858
  }
376859
+ case "tools:getPlugins": {
376860
+ if (!this.options.toolsBridge?.getPlugins) {
376861
+ throw new WebSocketRpcError(
376862
+ "METHOD_NOT_FOUND",
376863
+ "tools:getPlugins is not available on this websocket gateway."
376864
+ );
376865
+ }
376866
+ return await this.options.toolsBridge.getPlugins();
376867
+ }
376652
376868
  case "tools:setBuiltInEnabled": {
376653
376869
  if (!this.options.toolsBridge?.setBuiltInEnabled) {
376654
376870
  throw new WebSocketRpcError(
@@ -392577,6 +392793,23 @@ var PluginRegistry = class {
392577
392793
  allTools() {
392578
392794
  return this.list().filter((p) => p.enabled && !p.error).flatMap((p) => p.tools);
392579
392795
  }
392796
+ /** Flatten enabled plugin tools for AgentService.setPluginTools. */
392797
+ collectAgentTools() {
392798
+ const out = [];
392799
+ for (const record2 of this.list()) {
392800
+ if (record2.error || !record2.enabled) continue;
392801
+ for (const tool2 of record2.tools) {
392802
+ out.push({
392803
+ name: tool2.name,
392804
+ description: tool2.description ?? "",
392805
+ params: tool2.params ?? {},
392806
+ handler: tool2.handler,
392807
+ plugin: record2.manifest.name
392808
+ });
392809
+ }
392810
+ }
392811
+ return out;
392812
+ }
392580
392813
  /** All triggers from enabled plugins. */
392581
392814
  allTriggers() {
392582
392815
  return this.list().filter((p) => p.enabled && !p.error).flatMap((p) => p.triggers);
@@ -398016,25 +398249,15 @@ async function startGyBackend() {
398016
398249
  Promise.race([
398017
398250
  observability.pluginRegistry.reload(),
398018
398251
  new Promise((_, reject) => setTimeout(() => reject(new Error("plugin reload timeout (10s)")), 1e4))
398019
- ]).then((pluginRecords) => {
398020
- const pluginTools = [];
398021
- for (const record2 of pluginRecords) {
398022
- if (record2.error || !record2.enabled) continue;
398023
- for (const tool2 of record2.tools) {
398024
- pluginTools.push({
398025
- name: tool2.name,
398026
- description: tool2.description ?? "",
398027
- params: tool2.params ?? {},
398028
- handler: tool2.handler
398029
- });
398030
- }
398031
- }
398032
- if (pluginTools.length > 0) {
398033
- agentService.setPluginTools(pluginTools);
398034
- console.log(`[gybackend] Wired ${pluginTools.length} plugin tools from ${pluginRecords.filter((r) => !r.error && r.enabled).length} plugins into the agent.`);
398035
- } else {
398036
- console.log("[gybackend] No plugin tools found to wire.");
398252
+ ]).then(() => {
398253
+ const pluginTools = observability.pluginRegistry.collectAgentTools();
398254
+ agentService.setPluginTools(pluginTools);
398255
+ const enabled = observability.pluginRegistry.list().filter((r) => !r.error && r.enabled).length;
398256
+ try {
398257
+ gatewayService.broadcastRaw("tools:pluginsUpdated", agentService.listPluginTools());
398258
+ } catch {
398037
398259
  }
398260
+ console.log(`[gybackend] Wired ${pluginTools.length} plugin tools from ${enabled} plugins into the agent.`);
398038
398261
  }).catch((e) => {
398039
398262
  console.warn("[gybackend] Plugin tool wiring skipped:", e instanceof Error ? e.message : String(e));
398040
398263
  });
@@ -398698,6 +398921,7 @@ async function startGyBackend() {
398698
398921
  const settings = settingsService.getSettings();
398699
398922
  return buildBuiltInToolStatusSummary(settings.tools?.builtIn);
398700
398923
  },
398924
+ getPlugins: () => agentService.listPluginTools(),
398701
398925
  setBuiltInEnabled: async (name, enabled) => {
398702
398926
  const settings = settingsService.getSettings();
398703
398927
  const nextBuiltIn = { ...settings.tools?.builtIn ?? {} };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuralos",
3
- "version": "3.3.9",
3
+ "version": "3.4.1",
4
4
  "description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit.",
5
5
  "keywords": [
6
6
  "forward-deployed-engineer",