neuralos 3.4.0 → 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))
@@ -373404,16 +373479,84 @@ function buildUiSessionSummary(session) {
373404
373479
  }
373405
373480
 
373406
373481
  // ../../packages/backend/src/services/UIHistoryService.ts
373407
- var UIHistoryService = class {
373482
+ var UIHistoryService = class _UIHistoryService {
373408
373483
  store;
373409
373484
  sessionsCache = {};
373410
373485
  sessionSummaryCache = {};
373411
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;
373412
373505
  constructor(options) {
373413
373506
  this.store = options?.store || new HistorySqliteStore();
373414
373507
  this.sessionSummaryCache = this.buildSessionSummaryCache(
373415
373508
  this.store.listUiSessionSummaries()
373416
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
+ }
373417
373560
  }
373418
373561
  buildSessionSummaryCache(summaries) {
373419
373562
  const cache2 = {};
@@ -373433,6 +373576,9 @@ var UIHistoryService = class {
373433
373576
  }
373434
373577
  const sanitized = sanitizeUiSession(loaded);
373435
373578
  this.sessionsCache[sessionId] = sanitized;
373579
+ if (!this.persistedMessageCount.has(sessionId)) {
373580
+ this.persistedMessageCount.set(sessionId, sanitized.messages.length);
373581
+ }
373436
373582
  this.syncSessionSummary(sessionId);
373437
373583
  return sanitized;
373438
373584
  }
@@ -373459,6 +373605,7 @@ var UIHistoryService = class {
373459
373605
  const actions = this.processEvent(session, event, sessionId);
373460
373606
  this.syncSessionSummary(sessionId);
373461
373607
  this.dirtySessions.add(sessionId);
373608
+ this.scheduleFlush();
373462
373609
  return actions;
373463
373610
  }
373464
373611
  flush(sessionId) {
@@ -373467,6 +373614,7 @@ var UIHistoryService = class {
373467
373614
  return;
373468
373615
  }
373469
373616
  const entries = [];
373617
+ const appendFrom = {};
373470
373618
  sessionIds.forEach((id) => {
373471
373619
  const session = this.sessionsCache[id];
373472
373620
  if (!session) {
@@ -373478,9 +373626,34 @@ var UIHistoryService = class {
373478
373626
  const summary = buildUiSessionSummary(sanitized);
373479
373627
  this.sessionSummaryCache[id] = summary;
373480
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;
373481
373631
  });
373482
373632
  if (entries.length > 0) {
373483
- 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
+ }
373484
373657
  }
373485
373658
  sessionIds.forEach((id) => this.dirtySessions.delete(id));
373486
373659
  }
@@ -373993,6 +374166,7 @@ Error: ${event.message}` : ""),
373993
374166
  delete this.sessionsCache[id];
373994
374167
  delete this.sessionSummaryCache[id];
373995
374168
  this.dirtySessions.delete(id);
374169
+ this.persistedMessageCount.delete(id);
373996
374170
  });
373997
374171
  this.store.deleteUiSessions(ids);
373998
374172
  }
@@ -374031,6 +374205,7 @@ Error: ${event.message}` : ""),
374031
374205
  );
374032
374206
  this.syncSessionSummary(sessionId);
374033
374207
  this.dirtySessions.add(sessionId);
374208
+ this.persistedMessageCount.delete(sessionId);
374034
374209
  this.flush(sessionId);
374035
374210
  return removedCount;
374036
374211
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuralos",
3
- "version": "3.4.0",
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",
Binary file