querysub 0.650.0 → 0.652.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.
package/overview.md ADDED
@@ -0,0 +1,46 @@
1
+ # Querysub Overview
2
+
3
+ The system is fundamentally a data synchronization system.
4
+
5
+ ## Path values and time
6
+
7
+ Data exists in path values, which have a path (a structured list of strings) and a value. These are time based: they have the time that they occur at, which is a globally unique time. This time includes a somewhat unique identifier for the server that created the time (best effort — it cannot be relied on, but is usually unique), the time itself, and also a version value that our system uses to create something that happens at the same time, but just an epsilon amount after.
8
+
9
+ ## Locks and validity
10
+
11
+ Path values also include what are referred to as locks. Locks are essentially a list of reads that were done at the time the path value was created.
12
+
13
+ Values are evaluated in an eventually consistent manner. A value can be rejected if its locks are rejected. This can mean that the value it read has itself become rejected, or it can mean that the value it read was not the latest value at the time — which we know if we find another value between the creation time of the value that was read and the time that it was read at. That's why it's called a lock: it's a kind of locking of that value between its creation time and the time we read it. Of course, it's a reversed lock — instead of having the writer fail because of the lock, we have the reader fail.
14
+
15
+ ## Synchronization and watchers
16
+
17
+ Synchronization is very important. We synchronize values between different watchers in a client, and between machines. This is done at a low level in PathWatcher and RemoteWatcher. We have the ability to watch a path, or watch the direct children of a path. There is no recursive watching — if values need to be recursively watched, the watcher has to recursively access more and more each time it receives values, watching more each time.
18
+
19
+ ## Schema
20
+
21
+ We have a schema system which can simplify accesses and make it so that if values aren't provided, we know whether to default to a primitive value or use a proxy so the code can drill down further.
22
+
23
+ ## Proxy watcher
24
+
25
+ Most accesses are done using the proxy watcher. This is a system that sits on top of our other systems, letting you write regular JavaScript code to access fields and write to fields as if it were plain JavaScript, with all of the reads and writes being converted to path value reads and writes.
26
+
27
+ ## Client writes, function calls, and predictions
28
+
29
+ On the client, anything that needs to impact a remote value is mutated by making a special socket function call to the server, asking it to add a function call write — which writes a path value to a specific location. We then have a function runner server that reads from that location, running those functions. The client side also runs these functions, but only as a prediction. That way most actions feel immediate. When the server function runner does eventually run, it updates with the actual results.
30
+
31
+ ## Sharding
32
+
33
+ The servers are sharded — they are split up over different values. We generally don't do global path hashing. Instead, there are specific paths that a server reads on startup, and it does its hashing by looking at the child key of those paths when they match. These generally match up with lookups, so the hashes are usually the lookup keys. This allows an object's fields to be on the same server — minimizing the number of different servers we access — while still sharding the data.
34
+
35
+ ## Geographic routing
36
+
37
+ Almost everything has some level of sharding or geographic control. For example, the client side tries to connect to the querysub server — the server that actually adds the function calls and interacts with the client — and it usually connects to the closest one, or the one with the lowest latency. Function runners are also sharded between geographic locations, and the client usually requests one that is close to it.
38
+
39
+ ## Non-core systems
40
+
41
+ - The binary format of path values, which involves compression, etc.
42
+ - The error notification system.
43
+ - The logging and log search system.
44
+ - The MCP server.
45
+ - QReact, which implements a JSX component renderer using our synchronization system.
46
+ - A machine and service management system.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.650.0",
3
+ "version": "0.652.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -31,7 +31,7 @@ import { ProcessRecord, syncProcessRecords, listProcessRecords } from "./process
31
31
 
32
32
 
33
33
 
34
- const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ value: number; max: number } | undefined> {
34
+ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ memory?: { value: number; max: number }; swap?: { value: number; max: number } } | undefined> {
35
35
  if (os.platform() === "win32") {
36
36
  throw new Error("Windows is not supported for machine resource monitoring");
37
37
  }
@@ -40,17 +40,24 @@ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ valu
40
40
  // Linux: Use free command
41
41
  let result = await runPromise("free -b", { quiet: true });
42
42
  let lines = result.split("\n");
43
- let memLine = lines.find(line => line.startsWith("Mem:"));
44
- if (memLine) {
45
- let parts = memLine.split(/\s+/);
43
+ let parseLine = (prefix: string): { value: number; max: number } | undefined => {
44
+ let line = lines.find(line => line.startsWith(prefix));
45
+ if (!line) return undefined;
46
+ let parts = line.split(/\s+/);
46
47
  let total = parseInt(parts[1]);
47
48
  let used = parseInt(parts[2]);
48
- if (total && used >= 0) {
49
+ if (total >= 0 && used >= 0) {
49
50
  return { value: used, max: total };
50
51
  }
52
+ return undefined;
53
+ };
54
+ let memory = parseLine("Mem:");
55
+ let swap = parseLine("Swap:");
56
+ if (memory || swap) {
57
+ return { memory, swap };
51
58
  }
52
59
  } catch (e: any) {
53
- console.warn(`Error getting memory info: ${e.message}`);
60
+ console.warn(`Error getting memory info: ${e.stack}`);
54
61
  }
55
62
  return undefined;
56
63
  });
@@ -98,11 +105,19 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
98
105
  getDiskInfo()
99
106
  ]);
100
107
 
101
- if (memoryInfo) {
108
+ if (memoryInfo?.memory) {
102
109
  machineInfo.info.ram = {
103
110
  type: "MEMORY",
104
- value: memoryInfo.value,
105
- max: memoryInfo.max,
111
+ value: memoryInfo.memory.value,
112
+ max: memoryInfo.memory.max,
113
+ };
114
+ }
115
+
116
+ if (memoryInfo?.swap) {
117
+ machineInfo.info.swap = {
118
+ type: "SWAP",
119
+ value: memoryInfo.swap.value,
120
+ max: memoryInfo.swap.max,
106
121
  };
107
122
  }
108
123
 
@@ -20,11 +20,14 @@ import { MCPIndexedLogs, normalizeTime } from "./MCPIndexedLogs";
20
20
  import { searchStorageLogsForMCP } from "../../../deployManager/components/storage/storageLogMCPSearch";
21
21
  import { getAllNodeIds } from "../../../-f-node-discovery/NodeDiscovery";
22
22
  import { NodeCapabilitiesController } from "../../../-g-core-values/NodeCapabilities";
23
- import { formatTime } from "socket-function/src/formatting/format";
23
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
24
24
  import { SocketFunction } from "socket-function/SocketFunction";
25
+ import { sort } from "socket-function/src/misc";
26
+ import { getSuppressionEntries } from "../errorNotifications2/errorNotifications";
25
27
 
26
28
  const DEFAULT_MCP_HTTP_PORT = 4487;
27
29
  const NODE_INFO_TIMEOUT_MS = 5000;
30
+ const DEFAULT_SUPPRESSION_LIMIT = 20;
28
31
 
29
32
  const PROTOCOL_VERSION = "2025-03-26";
30
33
  const SERVER_INFO = { name: "querysub-indexed-logs", version: "0.1.0" };
@@ -97,6 +100,19 @@ Query syntax (case-sensitive substring match against each entry's JSON):
97
100
  properties: {},
98
101
  },
99
102
  },
103
+ {
104
+ name: "getSuppressions",
105
+ description: `List error-suppression entries (patterns that stop matching errors from triggering notifications), sorted most-recently-updated first. There can be hundreds, so results are limited (default ${DEFAULT_SUPPRESSION_LIMIT}); use the optional query filter to narrow.
106
+
107
+ Returns { total, returned, results } — total is the entry count after filtering, results contains { id, pattern, notes, lastUpdated, created, timeout, expired }. All three times are formatted date strings. expired is present (true) when the entry's timeout has passed, meaning it no longer suppresses anything.`,
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ limit: { type: "number", default: DEFAULT_SUPPRESSION_LIMIT, description: "Maximum entries to return, most recently updated first." },
112
+ query: { type: "string", description: "Optional case-insensitive substring filter, matched against id, pattern, and notes." },
113
+ },
114
+ },
115
+ },
100
116
  ];
101
117
 
102
118
  type JsonRpcRequest = {
@@ -184,6 +200,8 @@ async function dispatch(method: string, params: unknown, mcp: MCPIndexedLogs): P
184
200
  });
185
201
  } else if (toolName === "listNodes") {
186
202
  result = await getNodeInfos();
203
+ } else if (toolName === "getSuppressions") {
204
+ result = await getSuppressionsForMCP(args as { limit?: number; query?: string });
187
205
  } else {
188
206
  throw new Error(`Unknown tool ${toolName}`);
189
207
  }
@@ -220,6 +238,34 @@ async function getNodeInfos(): Promise<NodeInfo[]> {
220
238
  );
221
239
  }
222
240
 
241
+ async function getSuppressionsForMCP(config: { limit?: number; query?: string }) {
242
+ let limit = config.limit ?? DEFAULT_SUPPRESSION_LIMIT;
243
+ let entries = await getSuppressionEntries();
244
+ let query = config.query?.toLowerCase();
245
+ if (query) {
246
+ const q = query;
247
+ entries = entries.filter(x => (x.id + "\n" + x.pattern + "\n" + (x.notes || "")).toLowerCase().includes(q));
248
+ }
249
+ sort(entries, x => -x.lastUpdatedTime);
250
+ let now = Date.now();
251
+ let results = entries.slice(0, limit).map(x => {
252
+ let expired: true | undefined;
253
+ if (x.timeout < now) {
254
+ expired = true;
255
+ }
256
+ return {
257
+ id: x.id,
258
+ pattern: x.pattern,
259
+ notes: x.notes,
260
+ lastUpdated: formatDateTime(x.lastUpdatedTime),
261
+ created: formatDateTime(x.createdTime),
262
+ timeout: formatDateTime(x.timeout),
263
+ expired,
264
+ };
265
+ });
266
+ return { total: entries.length, returned: results.length, results };
267
+ }
268
+
223
269
  async function main() {
224
270
  let mcp = new MCPIndexedLogs();
225
271
 
@@ -394,22 +394,27 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
394
394
  continue;
395
395
  }
396
396
 
397
+ let responseMissing = !response.time || compareTime(response.time, epochTime) === 0;
398
+ if (responseMissing) {
399
+ response = { ...response, valid: true, isTransparent: true };
400
+ }
397
401
  if (ourValue.isTransparent && response.isTransparent) continue;
398
402
 
399
403
  if (request.time) {
400
404
  let ourExact = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
401
405
  if (!ourExact || !ourExact.valid) continue;
402
406
  if (compareTime(ourExact.time, epochTime) === 0) continue;
407
+ if (ourExact.isTransparent && responseMissing) continue;
403
408
  // The exact value = server does not have
404
409
  // - Send it our value
405
- if (!response.time) {
410
+ if (!response.time || responseMissing) {
406
411
  valuesToSend.push(ourExact);
407
412
  trackSyncAge({
408
413
  path: request.path,
409
414
  ourTimeId: ourExact.time.time,
410
415
  remoteTimeId: undefined,
411
416
  ourValid: ourExact.valid,
412
- remoteValid: response.valid,
417
+ remoteValid: undefined,
413
418
  remoteNodeId: nodeId,
414
419
  reason: "Remote is missing our value, sending it to them",
415
420
  });
@@ -471,14 +476,14 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
471
476
  }
472
477
  // Our latest valid = server does not have & it's latest valid is older than ours
473
478
  // - Send it our value
474
- else if (response.valid === undefined && (!response.time || compareTime(ourValue.time, response.time) > 0)) {
479
+ else if (responseMissing && compareTime(ourValue.time, epochTime) > 0) {
475
480
  valuesToSend.push(ourValue);
476
481
  trackSyncAge({
477
482
  path: response.path,
478
483
  ourTimeId: ourValue.time.time,
479
- remoteTimeId: response.time?.time,
484
+ remoteTimeId: undefined,
480
485
  ourValid: ourValue.valid ?? false,
481
- remoteValid: response.valid,
486
+ remoteValid: undefined,
482
487
  remoteNodeId: nodeId,
483
488
  reason: "Remote is missing our value, sending it to them",
484
489
  });
@@ -535,12 +540,13 @@ class PathAuditerService {
535
540
  } else {
536
541
  value = authorityStorage.getValueAtOrBeforeTime(request.path);
537
542
  }
543
+ value = value || createMissingEpochValue(request.path);
538
544
  results.push({
539
545
  path: request.path,
540
- time: value?.time,
541
- valid: value === undefined ? undefined : value.valid,
542
- isTransparent: !!value?.isTransparent,
543
- event: !!value?.event,
546
+ time: value.time,
547
+ valid: value.valid,
548
+ isTransparent: !!value.isTransparent,
549
+ event: !!value.event,
544
550
  });
545
551
  }
546
552
  return results;
@@ -8,8 +8,11 @@ export const MEMORY_WARNING_THRESHOLD = 0.7;
8
8
  export const MEMORY_ERROR_THRESHOLD = 0.85;
9
9
  export const DISK_WARNING_THRESHOLD = 0.8;
10
10
  export const DISK_ERROR_THRESHOLD = 0.92;
11
+ // Swap in use at all means memory pressure already happened, so warn much earlier than RAM.
12
+ export const SWAP_WARNING_THRESHOLD = 0.25;
13
+ export const SWAP_ERROR_THRESHOLD = 0.7;
11
14
 
12
- /** The standard thresholds for the machine resource bar types ("MEMORY" / "DISK"), so every page showing these bars warns identically. */
15
+ /** The standard thresholds for the machine resource bar types ("MEMORY" / "DISK" / "SWAP"), so every page showing these bars warns identically. */
13
16
  export function getUsageThresholds(type: string): { warningThreshold?: number; errorThreshold?: number } {
14
17
  if (type === "MEMORY") {
15
18
  return { warningThreshold: MEMORY_WARNING_THRESHOLD, errorThreshold: MEMORY_ERROR_THRESHOLD };
@@ -17,6 +20,9 @@ export function getUsageThresholds(type: string): { warningThreshold?: number; e
17
20
  if (type === "DISK") {
18
21
  return { warningThreshold: DISK_WARNING_THRESHOLD, errorThreshold: DISK_ERROR_THRESHOLD };
19
22
  }
23
+ if (type === "SWAP") {
24
+ return { warningThreshold: SWAP_WARNING_THRESHOLD, errorThreshold: SWAP_ERROR_THRESHOLD };
25
+ }
20
26
  return {};
21
27
  }
22
28