neuralos 3.3.2 → 3.3.4

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
@@ -256134,6 +256134,10 @@ var init_schedulerService = __esm({
256134
256134
  } catch {
256135
256135
  }
256136
256136
  }
256137
+ try {
256138
+ this.opts.onMinute?.(new Date(cur));
256139
+ } catch {
256140
+ }
256137
256141
  cur = new Date(cur.getTime() + 6e4);
256138
256142
  }
256139
256143
  this.lastTickMs = nowMs;
@@ -348672,6 +348676,125 @@ var AgentRunLedger = class {
348672
348676
  }
348673
348677
  };
348674
348678
 
348679
+ // ../../packages/backend/src/services/AgentHelper/utils/safeMemorySaver.ts
348680
+ var CHECKPOINT_STRING_CAP = 8 * 1024;
348681
+ var CHECKPOINT_BYTES_CAP = 16 * 1024 * 1024;
348682
+ var CHECKPOINT_MAX_MESSAGES = 40;
348683
+ function truncateString(input, max = CHECKPOINT_STRING_CAP) {
348684
+ if (input.length <= max) return input;
348685
+ const omitted = input.length - max;
348686
+ return `${input.slice(0, max)}
348687
+ ...[truncated ${omitted} chars for checkpoint]...`;
348688
+ }
348689
+ function pruneValue(value, depth = 0) {
348690
+ if (depth > 8) return "[pruned: depth]";
348691
+ if (typeof value === "string") return truncateString(value);
348692
+ if (Array.isArray(value)) {
348693
+ return value.map((item) => pruneValue(item, depth + 1));
348694
+ }
348695
+ if (value && typeof value === "object") {
348696
+ const obj = value;
348697
+ const out = {};
348698
+ for (const [k, v] of Object.entries(obj)) {
348699
+ out[k] = pruneValue(v, depth + 1);
348700
+ }
348701
+ return out;
348702
+ }
348703
+ return value;
348704
+ }
348705
+ function pruneMessagesArray(messages) {
348706
+ const pruned = messages.map((m2) => pruneValue(m2));
348707
+ if (pruned.length <= CHECKPOINT_MAX_MESSAGES) return pruned;
348708
+ const keep = CHECKPOINT_MAX_MESSAGES - 1;
348709
+ const dropped = pruned.length - keep;
348710
+ return [
348711
+ {
348712
+ type: "system",
348713
+ content: `[checkpoint] dropped ${dropped} older messages to stay under memory cap`
348714
+ },
348715
+ ...pruned.slice(-keep)
348716
+ ];
348717
+ }
348718
+ function pruneCheckpointForSaver(checkpoint) {
348719
+ if (!checkpoint || typeof checkpoint !== "object") return checkpoint;
348720
+ const cp = checkpoint;
348721
+ const next = { ...cp };
348722
+ const channelValues = cp.channel_values;
348723
+ if (channelValues && typeof channelValues === "object") {
348724
+ const channels = { ...channelValues };
348725
+ if (Array.isArray(channels.messages)) {
348726
+ channels.messages = pruneMessagesArray(channels.messages);
348727
+ }
348728
+ for (const [k, v] of Object.entries(channels)) {
348729
+ if (k === "messages") continue;
348730
+ channels[k] = pruneValue(v);
348731
+ }
348732
+ next.channel_values = channels;
348733
+ }
348734
+ if ("pending_sends" in next) {
348735
+ next.pending_sends = pruneValue(next.pending_sends);
348736
+ }
348737
+ return next;
348738
+ }
348739
+ function isAllocationError(error40) {
348740
+ if (!error40) return false;
348741
+ const msg = error40 instanceof Error ? error40.message : String(error40);
348742
+ const name = error40 instanceof Error ? error40.name : "";
348743
+ return name === "RangeError" || /failed to allocate memory|invalid string length|array buffer allocation failed|Cannot allocate|ERR_STRING_TOO_LONG/i.test(
348744
+ msg
348745
+ );
348746
+ }
348747
+ var SafeMemorySaver = class extends MemorySaver {
348748
+ async put(config2, checkpoint, metadata) {
348749
+ const pruned = pruneCheckpointForSaver(checkpoint);
348750
+ try {
348751
+ return await super.put(config2, pruned, pruneValue(metadata));
348752
+ } catch (error40) {
348753
+ if (!isAllocationError(error40)) throw error40;
348754
+ console.warn(
348755
+ "[SafeMemorySaver] checkpoint serialize OOM \u2014 dropping older messages and retrying"
348756
+ );
348757
+ try {
348758
+ const tighter = pruneCheckpointForSaver(pruned);
348759
+ const channels = tighter.channel_values ?? {};
348760
+ if (Array.isArray(channels.messages)) {
348761
+ channels.messages = channels.messages.slice(-8);
348762
+ }
348763
+ tighter.channel_values = channels;
348764
+ return await super.put(config2, tighter, { source: "oom-pruned" });
348765
+ } catch (retryError) {
348766
+ if (!isAllocationError(retryError)) throw retryError;
348767
+ console.warn(
348768
+ "[SafeMemorySaver] checkpoint still too large \u2014 skipping persist so the run can finish"
348769
+ );
348770
+ return {
348771
+ configurable: {
348772
+ thread_id: config2?.configurable?.thread_id,
348773
+ checkpoint_ns: config2?.configurable?.checkpoint_ns ?? "",
348774
+ checkpoint_id: checkpoint?.id
348775
+ }
348776
+ };
348777
+ }
348778
+ }
348779
+ }
348780
+ async putWrites(config2, writes, taskId) {
348781
+ const prunedWrites = Array.isArray(writes) ? writes.map((w) => {
348782
+ if (Array.isArray(w) && w.length >= 2) {
348783
+ return [w[0], pruneValue(w[1]), ...w.slice(2)];
348784
+ }
348785
+ return pruneValue(w);
348786
+ }) : writes;
348787
+ try {
348788
+ return await super.putWrites(config2, prunedWrites, taskId);
348789
+ } catch (error40) {
348790
+ if (!isAllocationError(error40)) throw error40;
348791
+ console.warn(
348792
+ "[SafeMemorySaver] putWrites serialize OOM \u2014 skipping writes persist"
348793
+ );
348794
+ }
348795
+ }
348796
+ };
348797
+
348675
348798
  // ../../packages/backend/src/services/AgentHelper/tools/edit_tools.ts
348676
348799
  init_zod();
348677
348800
 
@@ -369354,13 +369477,17 @@ var AgentService_v2 = class {
369354
369477
  this.imageAttachmentService = imageAttachmentService || null;
369355
369478
  this.fileTransferService = fileTransferService || null;
369356
369479
  this.helpers = new AgentHelpers();
369357
- this.checkpointer = new MemorySaver();
369480
+ this.checkpointer = new SafeMemorySaver();
369358
369481
  this.initializeGraph();
369359
369482
  }
369360
369483
  updateSettings(settings) {
369361
369484
  this.settings = settings;
369362
369485
  this.builtInToolEnabled = settings.tools?.builtIn ?? {};
369363
369486
  this.initializeGraph();
369487
+ try {
369488
+ this.triggerEngine?.reloadFrom(settings.automation?.triggers ?? []);
369489
+ } catch {
369490
+ }
369364
369491
  }
369365
369492
  setEventPublisher(publisher) {
369366
369493
  this.helpers.setEventPublisher(publisher);
@@ -369444,15 +369571,18 @@ var AgentService_v2 = class {
369444
369571
  return this.helpers.isAbortError(error40);
369445
369572
  }
369446
369573
  /**
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.
369574
+ * v3.2.18 / v3.3.3: resolve a fallback model (id, name, or provider model
369575
+ * string) to its ModelDefinition. Must use `this.settings` the previous
369576
+ * `this.options.getSettings` path never existed, so failover always missed.
369577
+ * Prefers an item that has an API key when several share the same model id.
369449
369578
  */
369450
369579
  resolveFallbackModelItem(modelId) {
369451
369580
  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;
369581
+ const items = this.settings?.models?.items ?? [];
369582
+ if (!modelId) return null;
369583
+ const keyed = items.filter((m2) => !!m2.apiKey);
369584
+ const pool = keyed.length ? keyed : items;
369585
+ return pool.find((m2) => m2.id === modelId) ?? pool.find((m2) => m2.model === modelId) ?? pool.find((m2) => m2.name === modelId) ?? null;
369456
369586
  } catch {
369457
369587
  return null;
369458
369588
  }
@@ -370021,71 +370151,44 @@ var AgentService_v2 = class {
370021
370151
  let reasoningContent = "";
370022
370152
  let debugRawChunks = [];
370023
370153
  const llmTraceStart = Date.now();
370024
- const fallbacks = sessionBinding.fallbackModels;
370154
+ const fallbacks = sessionBinding.fallbackModels ?? [];
370025
370155
  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
370068
- });
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
- }
370156
+ const { withModelFailover: withModelFailover2, buildFailoverChain: buildFailoverChain2 } = await Promise.resolve().then(() => (init_modelFailover(), modelFailover_exports));
370157
+ const primaryModel = baseModel?.model ?? baseModel?.modelName ?? "unknown";
370158
+ const chain = buildFailoverChain2(
370159
+ { model: primaryModel },
370160
+ fallbacks.map((f) => ({ model: f.model, label: f.label }))
370161
+ );
370162
+ const failover = await withModelFailover2(chain, async (candidate) => {
370163
+ let modelToUse = modelWithTools;
370164
+ if (candidate.model !== primaryModel) {
370165
+ partialText = "";
370166
+ reasoningContent = "";
370167
+ debugRawChunks = [];
370168
+ const fallbackItem = this.resolveFallbackModelItem(candidate.model);
370169
+ if (!fallbackItem || !fallbackItem.apiKey) {
370170
+ throw new Error(`fallback model "${candidate.model}" is not configured or has no API key`);
370074
370171
  }
370075
- });
370076
- if (failover.value !== void 0) {
370077
- fullResponse = failover.value;
370078
- } else {
370079
- throw failover.error ?? new Error("model failover exhausted all candidates");
370172
+ this.helpers.sendEvent(sessionId, {
370173
+ type: "alert",
370174
+ message: `Primary model failed \u2014 failing over to ${candidate.label || candidate.model}`,
370175
+ level: "warning",
370176
+ messageId: `failover-${messageId}-${candidate.model}`
370177
+ });
370178
+ const fallbackChat = this.helpers.createChatModel(
370179
+ fallbackItem,
370180
+ shouldUseThinkingModelOnThisPass ? 0.2 : 0.7,
370181
+ null
370182
+ );
370183
+ modelToUse = fallbackChat.bindTools([...builtInTools, ...mcpTools]);
370080
370184
  }
370081
- } else {
370082
- fullResponse = await invokeWithRetryAndSanitizedInput({
370185
+ return await invokeWithRetryAndSanitizedInput({
370083
370186
  helpers: this.helpers,
370084
370187
  messages: modelInputMessages,
370085
370188
  modelSupportsImage: sessionBinding.readFileSupport.image,
370086
370189
  signal: config2?.signal,
370087
370190
  operation: async (streamInputMessages) => {
370088
- const stream = await modelWithTools.stream(streamInputMessages, {
370191
+ const stream = await modelToUse.stream(streamInputMessages, {
370089
370192
  signal: config2?.signal
370090
370193
  });
370091
370194
  let response = null;
@@ -370245,6 +370348,19 @@ var AgentService_v2 = class {
370245
370348
  maxRetries: MODEL_RETRY_MAX,
370246
370349
  delaysMs: MODEL_RETRY_DELAYS_MS
370247
370350
  });
370351
+ }, {
370352
+ onAttempt: (a) => {
370353
+ if (!a.ok) {
370354
+ console.warn(`[AgentService_v2] model failover: ${a.model} failed (${a.reason}) after ${a.durationMs}ms`);
370355
+ } else if (a.model !== primaryModel) {
370356
+ console.log(`[AgentService_v2] model failover: succeeded on ${a.model} after ${a.durationMs}ms`);
370357
+ }
370358
+ }
370359
+ });
370360
+ if (failover.value !== void 0) {
370361
+ fullResponse = failover.value;
370362
+ } else {
370363
+ throw failover.error ?? new Error("model failover exhausted all candidates");
370248
370364
  }
370249
370365
  fullResponse.additional_kwargs = {
370250
370366
  ...fullResponse.additional_kwargs || {},
@@ -386353,8 +386469,9 @@ function normalizePlaybookSteps(steps) {
386353
386469
  if (!steps || steps.length === 0) throw new Error("A playbook needs at least one step");
386354
386470
  return steps.map((s, idx) => {
386355
386471
  const kind = s.kind;
386356
- if (kind !== "command" && kind !== "script" && kind !== "wait") {
386357
- throw new Error(`Step ${idx + 1}: kind must be command|script|wait`);
386472
+ const allowed = ["command", "script", "wait", "template", "playbook", "healthCheck"];
386473
+ if (!allowed.includes(kind)) {
386474
+ throw new Error(`Step ${idx + 1}: kind must be ${allowed.join("|")}`);
386358
386475
  }
386359
386476
  if (kind === "command" && !(s.command ?? "").trim()) {
386360
386477
  throw new Error(`Step ${idx + 1}: command steps need a non-empty command`);
@@ -386365,6 +386482,15 @@ function normalizePlaybookSteps(steps) {
386365
386482
  if (kind === "wait" && !(typeof s.waitSeconds === "number" && s.waitSeconds > 0)) {
386366
386483
  throw new Error(`Step ${idx + 1}: wait steps need waitSeconds > 0`);
386367
386484
  }
386485
+ if (kind === "template" && !(s.templateId ?? "").trim()) {
386486
+ throw new Error(`Step ${idx + 1}: template steps need a templateId`);
386487
+ }
386488
+ if (kind === "playbook" && !(s.playbookId ?? "").trim()) {
386489
+ throw new Error(`Step ${idx + 1}: playbook steps need a playbookId`);
386490
+ }
386491
+ if (kind === "healthCheck" && !(s.healthUrl ?? "").trim()) {
386492
+ throw new Error(`Step ${idx + 1}: healthCheck steps need a healthUrl`);
386493
+ }
386368
386494
  if (s.onError && s.onError !== "stop" && s.onError !== "continue") {
386369
386495
  throw new Error(`Step ${idx + 1}: onError must be stop|continue`);
386370
386496
  }
@@ -387867,6 +387993,7 @@ var AgentSettingProfileService = class {
387867
387993
 
387868
387994
  // ../../packages/backend/src/services/automation/triggerEngine.ts
387869
387995
  var import_crypto10 = require("crypto");
387996
+ init_schedulerService();
387870
387997
  var DEFAULT_COOLDOWN_S = 300;
387871
387998
  var HISTORY_LIMIT_DEFAULT2 = 200;
387872
387999
  var MAX_CONCURRENT_DEFAULT = 3;
@@ -387922,6 +388049,19 @@ var TriggerEngine = class {
387922
388049
  if (!t) return false;
387923
388050
  return this.triggers.delete(t.id);
387924
388051
  }
388052
+ /** Replace in-memory triggers from persisted settings (UI save path). */
388053
+ reloadFrom(list) {
388054
+ const previous = new Map(this.triggers);
388055
+ this.triggers.clear();
388056
+ for (const t of list) {
388057
+ const existing = previous.get(t.id);
388058
+ this.triggers.set(t.id, {
388059
+ ...t,
388060
+ fireCount: t.fireCount ?? existing?.fireCount ?? 0,
388061
+ lastFiredAt: t.lastFiredAt ?? existing?.lastFiredAt
388062
+ });
388063
+ }
388064
+ }
387925
388065
  setEnabled(idOrName, enabled) {
387926
388066
  const t = this.get(idOrName);
387927
388067
  if (!t) return false;
@@ -387956,6 +388096,25 @@ var TriggerEngine = class {
387956
388096
  }
387957
388097
  }
387958
388098
  }
388099
+ /**
388100
+ * Fire every enabled schedule-kind trigger whose cron matches `at`.
388101
+ * Called from SchedulerService on each minute tick.
388102
+ */
388103
+ fire_schedule(at) {
388104
+ const fired = [];
388105
+ for (const t of this.triggers.values()) {
388106
+ if (!t.enabled || t.kind !== "schedule") continue;
388107
+ if (!t.cron) continue;
388108
+ try {
388109
+ const due = t.timezone ? matchesCronInTz(t.cron, at, t.timezone) : matchesCron(t.cron, at);
388110
+ if (!due) continue;
388111
+ void this.fire(t, `schedule "${t.cron}" at ${at.toISOString()}`);
388112
+ fired.push(t.id);
388113
+ } catch {
388114
+ }
388115
+ }
388116
+ return fired;
388117
+ }
387959
388118
  /** Manually fire any webhook-kind triggers (or test a specific one). */
387960
388119
  fire_webhook(triggerIdOrName, reason) {
387961
388120
  const fired = [];
@@ -397541,6 +397700,12 @@ async function startGyBackend() {
397541
397700
  }
397542
397701
  const scheduler = new SchedulerService({
397543
397702
  getTasks: () => automationManager.listScheduledTasks(),
397703
+ onMinute: (at) => {
397704
+ try {
397705
+ triggerEngine.fire_schedule(at);
397706
+ } catch {
397707
+ }
397708
+ },
397544
397709
  onSkip: (task2, reason) => {
397545
397710
  if (reason === "overlap-skip") {
397546
397711
  console.warn(
Binary file
package/package.json CHANGED
@@ -1,11 +1,46 @@
1
1
  {
2
2
  "name": "neuralos",
3
- "version": "3.3.2",
4
- "description": "Headless AI-native backend for RTerm / neuralOS v3.3.2: playbook editor gains v3.2.17 fields (step timeout/retry/when/env, parallel targets, runtime cap, dry-run) + full Trigger manager UI. v3.3.0-1: configurable model failover + settings-coverage UI. 14 plugins, 61 plugin tools.",
5
- "main": "bin/gybackend.cjs",
6
- "bin": { "gybackend": "bin/gybackend.cjs" },
3
+ "version": "3.3.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
+ "keywords": [
6
+ "forward-deployed-engineer",
7
+ "fde",
8
+ "ai-terminal",
9
+ "ai-agent",
10
+ "ssh",
11
+ "winrm",
12
+ "serial-console",
13
+ "cisco",
14
+ "network-automation",
15
+ "sre",
16
+ "observability",
17
+ "incident-response",
18
+ "change-management",
19
+ "infrastructure-automation",
20
+ "runbooks",
21
+ "neuralos",
22
+ "aiops",
23
+ "ai-sre",
24
+ "agentic-ai",
25
+ "chatops",
26
+ "self-healing",
27
+ "runbook-automation",
28
+ "closed-loop-remediation",
29
+ "mcp"
30
+ ],
7
31
  "license": "MIT",
8
- "engines": { "node": ">=18" },
32
+ "homepage": "https://rterm.app",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/DrOlu/RTerm.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "bin": {
41
+ "gybackend": "bin/gybackend.cjs"
42
+ },
43
+ "main": "bin/gybackend.cjs",
9
44
  "dependencies": {
10
45
  "@nats-io/jetstream": "^3.4.0",
11
46
  "@nats-io/kv": "^3.4.0",