feat(daemon): _signals table retention policy (#152)
- Add `retention` field to SenseConfig (default 10000 max rows) - Parse optional `retention` positive integer in nerve.yaml sense config - Prune old _signals rows every 100 inserts for amortized performance - Pass retention from config through sense-worker to openSenseDb - Add unit tests for config parsing and runtime pruning
This commit is contained in:
@@ -181,7 +181,13 @@ const e2eRootCommand = defineCommand({
|
||||
function defaultTestConfig(withNoopWorkflow: boolean): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
counter: { group: "e2e", throttle: null, timeout: null, gracePeriod: null },
|
||||
counter: {
|
||||
group: "e2e",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {
|
||||
|
||||
@@ -96,6 +96,7 @@ async function runTestCli(fakeHome: string, args: string[]): Promise<CliRunResul
|
||||
} finally {
|
||||
process.exit = origExit;
|
||||
if (prevHome === undefined) {
|
||||
// biome-ignore lint/performance/noDelete: semantically correct for env cleanup
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = prevHome;
|
||||
|
||||
@@ -38,12 +38,14 @@ describe("parseNerveConfig", () => {
|
||||
throttle: 5000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
});
|
||||
expect(result.value.senses.memory).toEqual({
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: 10_000,
|
||||
gracePeriod: 3000,
|
||||
retention: 10_000,
|
||||
});
|
||||
expect(result.value.reflexes).toHaveLength(2);
|
||||
expect(result.value.reflexes[0]).toEqual({
|
||||
@@ -109,9 +111,24 @@ reflexes: []
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses optional retention as a positive integer", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
cpu:
|
||||
group: system
|
||||
retention: 5000
|
||||
reflexes: []
|
||||
`;
|
||||
const result = parseNerveConfig(yaml);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.senses.cpu.retention).toBe(5000);
|
||||
});
|
||||
|
||||
it("accepts all valid duration suffixes (s, m, h)", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
@@ -344,6 +361,48 @@ workflows:
|
||||
expect(result.error.message).toMatch(/workflow.*not supported/);
|
||||
});
|
||||
|
||||
it("returns error when retention is zero", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
cpu:
|
||||
group: system
|
||||
retention: 0
|
||||
reflexes: []
|
||||
`;
|
||||
const result = parseNerveConfig(yaml);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/retention.*positive integer/);
|
||||
});
|
||||
|
||||
it("returns error when retention is not an integer", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
cpu:
|
||||
group: system
|
||||
retention: 1.5
|
||||
reflexes: []
|
||||
`;
|
||||
const result = parseNerveConfig(yaml);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/retention.*positive integer/);
|
||||
});
|
||||
|
||||
it("returns error when retention is not a number", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
cpu:
|
||||
group: system
|
||||
retention: "5000"
|
||||
reflexes: []
|
||||
`;
|
||||
const result = parseNerveConfig(yaml);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/retention.*positive integer/);
|
||||
});
|
||||
|
||||
it("returns error for invalid throttle format", () => {
|
||||
const yaml = `
|
||||
senses:
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/** Default max rows kept in each sense's `_signals` SQLite table (see `retention` on `SenseConfig`). */
|
||||
export const DEFAULT_SENSE_SIGNAL_RETENTION = 10_000;
|
||||
|
||||
export type SenseConfig = {
|
||||
group: string;
|
||||
throttle: number | null;
|
||||
timeout: number | null;
|
||||
gracePeriod: number | null;
|
||||
/** Max rows to retain in `_signals`; older rows are pruned periodically after inserts. */
|
||||
retention: number;
|
||||
};
|
||||
|
||||
export type SenseReflexConfig = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { DEFAULT_SENSE_SIGNAL_RETENTION } from "./config.js";
|
||||
export type {
|
||||
SenseConfig,
|
||||
SenseReflexConfig,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { parse } from "yaml";
|
||||
|
||||
import type {
|
||||
NerveApiConfig,
|
||||
NerveConfig,
|
||||
ReflexConfig,
|
||||
SenseConfig,
|
||||
WorkflowConfig,
|
||||
import {
|
||||
DEFAULT_SENSE_SIGNAL_RETENTION,
|
||||
type NerveApiConfig,
|
||||
type NerveConfig,
|
||||
type ReflexConfig,
|
||||
type SenseConfig,
|
||||
type WorkflowConfig,
|
||||
} from "./config.js";
|
||||
import { isPlainRecord } from "./is-plain-record.js";
|
||||
import type { Result } from "./result.js";
|
||||
@@ -30,6 +31,16 @@ function isValidGroupName(value: string): boolean {
|
||||
return /^[a-zA-Z0-9_-]+$/.test(value);
|
||||
}
|
||||
|
||||
function parseRetentionField(name: string, field: unknown): Result<number> {
|
||||
if (field === undefined || field === null) {
|
||||
return ok(DEFAULT_SENSE_SIGNAL_RETENTION);
|
||||
}
|
||||
if (typeof field !== "number" || !Number.isInteger(field) || field < 1) {
|
||||
return err(new Error(`senses.${name}.retention: must be a positive integer`));
|
||||
}
|
||||
return ok(field);
|
||||
}
|
||||
|
||||
function parseDurationField(field: unknown, label: string): Result<number | null> {
|
||||
if (field === undefined || field === null) return ok(null);
|
||||
if (typeof field !== "string") {
|
||||
@@ -74,11 +85,15 @@ function validateSenseConfig(name: string, raw: unknown): Result<SenseConfig> {
|
||||
const graceResult = parseDurationField(obj.grace_period, `senses.${name}.grace_period`);
|
||||
if (!graceResult.ok) return graceResult;
|
||||
|
||||
const retentionResult = parseRetentionField(name, obj.retention);
|
||||
if (!retentionResult.ok) return retentionResult;
|
||||
|
||||
return ok({
|
||||
group: obj.group,
|
||||
throttle: throttleResult.value,
|
||||
timeout: timeoutResult.value,
|
||||
gracePeriod: graceResult.value,
|
||||
retention: retentionResult.value,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,13 @@ const MOCK_WORKER = join(__dir, "fixtures", "mock-worker.mjs");
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -75,9 +81,27 @@ describe("kernel integration — real child processes", () => {
|
||||
it("returns correct groups and senseCount", () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-io": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-io": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
kernel = createKernel(config, nerveRoot, {
|
||||
@@ -120,8 +144,20 @@ describe("kernel integration — real child processes", () => {
|
||||
it("graceful shutdown: stop() resolves after all workers exit", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
kernel = createKernel(config, nerveRoot, {
|
||||
|
||||
@@ -73,7 +73,13 @@ const { createKernel } = await import("../kernel.js");
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -104,9 +110,27 @@ describe("kernel — getHealth", () => {
|
||||
it("returns correct health shape", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const kernel = createKernel(config, nerveRoot);
|
||||
@@ -192,8 +216,20 @@ describe("kernel — reloadConfig", () => {
|
||||
|
||||
kernel.reloadConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -210,8 +246,20 @@ describe("kernel — reloadConfig", () => {
|
||||
it("removes group worker when its senses are all removed", async () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -227,7 +275,13 @@ describe("kernel — reloadConfig", () => {
|
||||
|
||||
kernel.reloadConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -250,8 +304,20 @@ describe("kernel — reloadConfig", () => {
|
||||
|
||||
kernel.reloadConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
|
||||
@@ -92,7 +92,13 @@ function makeMockLogStore() {
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -135,8 +141,20 @@ describe("kernel.triggerSense()", () => {
|
||||
it("sends a compute message to the worker for the correct group", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-io": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-io": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -165,8 +183,20 @@ describe("kernel.triggerSense()", () => {
|
||||
it("sends a compute message to the correct worker when multiple senses share a group", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
|
||||
@@ -95,7 +95,13 @@ function makeLogStore() {
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -129,7 +135,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "my-workflow": { concurrency: 2, overflow: "drop" } },
|
||||
@@ -173,7 +185,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "alert-workflow": { concurrency: 1, overflow: "drop" } },
|
||||
@@ -222,8 +240,20 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-io": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-io": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "my-workflow": { concurrency: 1, overflow: "drop" } },
|
||||
@@ -266,7 +296,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "log-test-workflow": { concurrency: 2, overflow: "drop" } },
|
||||
@@ -303,7 +339,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const initialConfig = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -318,7 +360,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
// Reload with a workflow added
|
||||
const newConfig: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "new-workflow": { concurrency: 1, overflow: "drop" } },
|
||||
@@ -358,7 +406,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const initialConfig = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "old-workflow": { concurrency: 1, overflow: "drop" } },
|
||||
@@ -372,7 +426,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
// Reload with the workflow removed
|
||||
const newConfig: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -418,7 +478,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "shutdown-test": { concurrency: 1, overflow: "drop" } },
|
||||
@@ -467,7 +533,13 @@ describe("kernel + workflowManager integration", () => {
|
||||
const logStore = makeLogStore();
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: { "health-wf": { concurrency: 2, overflow: "drop" } },
|
||||
|
||||
@@ -56,7 +56,13 @@ const { createLogStore } = await import("@uncaged/nerve-store");
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -87,7 +93,13 @@ describe("kernel — message routing", () => {
|
||||
it("routes signal message to bus without throwing", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -109,7 +121,13 @@ describe("kernel — message routing", () => {
|
||||
try {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -130,7 +148,13 @@ describe("kernel — message routing", () => {
|
||||
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -151,7 +175,13 @@ describe("kernel — message routing", () => {
|
||||
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -171,7 +201,13 @@ describe("kernel — message routing", () => {
|
||||
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
});
|
||||
@@ -204,9 +240,27 @@ describe("kernel — groupForSense mapping", () => {
|
||||
it("spawns one worker per unique group", async () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-usage": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-usage": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -223,7 +277,13 @@ describe("kernel — groupForSense mapping", () => {
|
||||
it("sends compute to the correct worker on interval trigger", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: 500, on: [] }],
|
||||
});
|
||||
|
||||
@@ -26,7 +26,13 @@ describe("LogStore + ReflexScheduler integration", () => {
|
||||
it("logs run_start when reflex triggers a compute", () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
workflows: {},
|
||||
@@ -55,7 +61,13 @@ describe("LogStore + ReflexScheduler integration", () => {
|
||||
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: 1000, on: [] }],
|
||||
workflows: {},
|
||||
@@ -87,7 +99,13 @@ describe("LogStore + ReflexScheduler integration", () => {
|
||||
it("logs cannot trigger reflexes (architectural constraint)", () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
workflows: {},
|
||||
|
||||
@@ -22,7 +22,13 @@ const ERROR_WORKER = join(__dir, "fixtures", "error-worker.mjs");
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -147,8 +153,20 @@ describe("phase6 — reloadConfig", () => {
|
||||
|
||||
const newConfig: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -168,8 +186,20 @@ describe("phase6 — reloadConfig", () => {
|
||||
it("removes group when all its senses are removed", async () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -185,7 +215,13 @@ describe("phase6 — reloadConfig", () => {
|
||||
|
||||
const newConfig: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -222,8 +258,20 @@ describe("phase6 — error isolation", () => {
|
||||
it("error from one sense does not crash the worker — other senses still work", async () => {
|
||||
const config: NerveConfig = {
|
||||
senses: {
|
||||
"good-sense": { group: "mixed", throttle: null, timeout: null, gracePeriod: null },
|
||||
"bad-sense": { group: "mixed", throttle: null, timeout: null, gracePeriod: null },
|
||||
"good-sense": {
|
||||
group: "mixed",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"bad-sense": {
|
||||
group: "mixed",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -303,9 +351,27 @@ describe("phase6 — getHealth", () => {
|
||||
it("returns health snapshot with correct shape", async () => {
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
kernel = createKernel(config, nerveRoot, {
|
||||
@@ -333,8 +399,20 @@ describe("phase6 — getHealth", () => {
|
||||
|
||||
const newConfig: NerveConfig = {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"net-rx": { group: "network", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"net-rx": {
|
||||
group: "network",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
|
||||
@@ -7,7 +7,13 @@ import { createSignalBus } from "../signal-bus.js";
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -34,7 +40,13 @@ describe("ReflexScheduler — throttle + pending deferred trigger", () => {
|
||||
const triggered: string[] = [];
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: 2000, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: 2000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
});
|
||||
@@ -63,7 +75,13 @@ describe("ReflexScheduler — throttle + pending deferred trigger", () => {
|
||||
const triggered: string[] = [];
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: 2000, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: 2000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
});
|
||||
@@ -96,7 +114,13 @@ describe("ReflexScheduler — throttle + pending deferred trigger", () => {
|
||||
const triggered: string[] = [];
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: 2000, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: 2000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
});
|
||||
|
||||
@@ -11,9 +11,27 @@ import { createSignalBus } from "../signal-bus.js";
|
||||
function makeConfig(overrides: Partial<NerveConfig> = {}): NerveConfig {
|
||||
return {
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"disk-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"system-health": { group: "derived", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"disk-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
"system-health": {
|
||||
group: "derived",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [],
|
||||
workflows: {},
|
||||
@@ -167,7 +185,13 @@ describe("ReflexScheduler — throttle", () => {
|
||||
const triggered: string[] = [];
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: 2000, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: 2000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
});
|
||||
@@ -189,7 +213,13 @@ describe("ReflexScheduler — throttle", () => {
|
||||
const triggered: string[] = [];
|
||||
const config = makeConfig({
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: 1000, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: 1000,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }],
|
||||
});
|
||||
@@ -294,7 +324,13 @@ describe("ReflexScheduler — workflow reflexes ignored", () => {
|
||||
const config: NerveConfig = {
|
||||
maxRounds: 10,
|
||||
senses: {
|
||||
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
|
||||
"cpu-usage": {
|
||||
group: "system",
|
||||
throttle: null,
|
||||
timeout: null,
|
||||
gracePeriod: null,
|
||||
retention: 10_000,
|
||||
},
|
||||
},
|
||||
reflexes: [{ kind: "workflow", workflow: "my-workflow", on: ["cpu-usage"] } as any],
|
||||
workflows: {
|
||||
|
||||
@@ -131,6 +131,24 @@ describe("openSenseDb", () => {
|
||||
const result = openSenseDb(dbPath, "/nonexistent/migrations");
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("prunes _signals to retention after every 100 inserts", () => {
|
||||
const dbPath = makeTempDbPath();
|
||||
const migrationsDir = makeTempMigrationsDir(INIT_SQL);
|
||||
const result = openSenseDb(dbPath, migrationsDir, 5);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
const { sqlite, persistSignal } = result.value;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
persistSignal({ n: i });
|
||||
}
|
||||
|
||||
const count = sqlite.prepare("SELECT COUNT(*) AS c FROM _signals").get() as { c: number };
|
||||
expect(count.c).toBe(5);
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -6,7 +6,7 @@ import { drizzle } from "drizzle-orm/node-sqlite";
|
||||
import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
||||
|
||||
import type { Result } from "@uncaged/nerve-core";
|
||||
import { err, isPlainRecord, ok } from "@uncaged/nerve-core";
|
||||
import { DEFAULT_SENSE_SIGNAL_RETENTION, err, isPlainRecord, ok } from "@uncaged/nerve-core";
|
||||
|
||||
import type { BlobStore } from "@uncaged/nerve-store";
|
||||
|
||||
@@ -129,9 +129,13 @@ export function runMigrations(sqlite: DatabaseSync, migrationsDir: string): Resu
|
||||
* Open (or create) the SQLite file at `dbPath`, run all migrations in
|
||||
* `migrationsDir`, and wrap with Drizzle ORM.
|
||||
*/
|
||||
/** Run `_signals` row prune after this many inserts (amortize DELETE cost). */
|
||||
const SIGNAL_INSERTS_PER_PRUNE = 100;
|
||||
|
||||
export function openSenseDb(
|
||||
dbPath: string,
|
||||
migrationsDir: string,
|
||||
retention: number = DEFAULT_SENSE_SIGNAL_RETENTION,
|
||||
): Result<{ sqlite: DatabaseSync; db: DrizzleDB; persistSignal: (payload: unknown) => void }> {
|
||||
let sqlite: DatabaseSync;
|
||||
|
||||
@@ -157,10 +161,20 @@ export function openSenseDb(
|
||||
);
|
||||
|
||||
const insertStmt = sqlite.prepare("INSERT INTO _signals (payload, timestamp) VALUES (?, ?)");
|
||||
const pruneStmt = sqlite.prepare(
|
||||
"DELETE FROM _signals WHERE id NOT IN (SELECT id FROM _signals ORDER BY id DESC LIMIT ?)",
|
||||
);
|
||||
|
||||
let insertsSincePrune = 0;
|
||||
|
||||
function persistSignal(payload: unknown): void {
|
||||
const json = JSON.stringify(payload);
|
||||
insertStmt.run(json, Date.now());
|
||||
insertsSincePrune += 1;
|
||||
if (insertsSincePrune >= SIGNAL_INSERTS_PER_PRUNE) {
|
||||
insertsSincePrune = 0;
|
||||
pruneStmt.run(retention);
|
||||
}
|
||||
}
|
||||
|
||||
// Drizzle infers a schema-specific DB type; senses are schema-agnostic at this layer.
|
||||
|
||||
@@ -75,12 +75,13 @@ function readConfig(nerveRoot: string): NerveConfig {
|
||||
async function initSense(
|
||||
nerveRoot: string,
|
||||
senseName: string,
|
||||
retention: number,
|
||||
): Promise<{ db: DrizzleDB; runtime: SenseRuntime }> {
|
||||
const dbPath = join(nerveRoot, "data", "senses", `${senseName}.db`);
|
||||
const migrationsDir = join(nerveRoot, "senses", senseName, "migrations");
|
||||
const senseIndexPath = resolve(join(nerveRoot, "senses", senseName, "index.js"));
|
||||
|
||||
const dbResult = openSenseDb(dbPath, migrationsDir);
|
||||
const dbResult = openSenseDb(dbPath, migrationsDir, retention);
|
||||
if (!dbResult.ok) {
|
||||
throw new Error(`Failed to init DB for "${senseName}": ${dbResult.error.message}`);
|
||||
}
|
||||
@@ -276,7 +277,8 @@ async function bootstrap(nerveRoot: string, group: string): Promise<void> {
|
||||
|
||||
for (const senseName of groupSenses) {
|
||||
try {
|
||||
const { db, runtime } = await initSense(nerveRoot, senseName);
|
||||
const retention = config.senses[senseName].retention;
|
||||
const { db, runtime } = await initSense(nerveRoot, senseName, retention);
|
||||
ownDbs.set(senseName, db);
|
||||
runtimes.set(senseName, runtime);
|
||||
} catch (e: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user