nemoris 0.1.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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
3
|
+
process.stdout.write = (chunk, ...args) => {
|
|
4
|
+
const text = typeof chunk === "string"
|
|
5
|
+
? chunk
|
|
6
|
+
: Buffer.isBuffer(chunk)
|
|
7
|
+
? chunk.toString("utf8")
|
|
8
|
+
: null;
|
|
9
|
+
|
|
10
|
+
if (text === null) {
|
|
11
|
+
return originalStdoutWrite(chunk, ...args);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filtered = text
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.filter((line) => !line.startsWith("[openclaw/patch]"))
|
|
17
|
+
.join("\n");
|
|
18
|
+
|
|
19
|
+
if (!filtered) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nextChunk = Buffer.isBuffer(chunk) ? Buffer.from(filtered, "utf8") : filtered;
|
|
24
|
+
return originalStdoutWrite(nextChunk, ...args);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const [{ default: path }, { default: fs }, { fileURLToPath }, { resolveEnvPath }] = await Promise.all([
|
|
28
|
+
import("node:path"),
|
|
29
|
+
import("node:fs"),
|
|
30
|
+
import("node:url"),
|
|
31
|
+
import("./utils/env-loader.js"),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function printVersionAndExit() {
|
|
35
|
+
const arg = process.argv[2];
|
|
36
|
+
if (arg === "--version" || arg === "-v" || arg === "version") {
|
|
37
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
38
|
+
const { version } = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
39
|
+
console.log(version);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ENV_BLOCKLIST = new Set(["NODE_OPTIONS", "LD_PRELOAD", "LD_LIBRARY_PATH", "DYLD_INSERT_LIBRARIES"]);
|
|
45
|
+
|
|
46
|
+
function loadParentEnv(envPath) {
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
49
|
+
for (const line of content.split("\n")) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
52
|
+
const eqIdx = trimmed.indexOf("=");
|
|
53
|
+
if (eqIdx < 1) continue;
|
|
54
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
55
|
+
if (ENV_BLOCKLIST.has(key)) continue;
|
|
56
|
+
if (process.env[key] !== undefined) continue;
|
|
57
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
58
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
process.env[key] = value;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// .env file is optional
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
printVersionAndExit();
|
|
69
|
+
|
|
70
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
71
|
+
const parentEnvPath = resolveEnvPath(__dirname);
|
|
72
|
+
loadParentEnv(parentEnvPath);
|
|
73
|
+
|
|
74
|
+
const { main } = await import("./cli-main.js");
|
|
75
|
+
const exitCode = await main(process.argv);
|
|
76
|
+
if (typeof exitCode === "number") {
|
|
77
|
+
process.exitCode = exitCode;
|
|
78
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { listFilesRecursive, readText } from "../utils/fs.js";
|
|
4
|
+
import { parseToml } from "./toml-lite.js";
|
|
5
|
+
import { validateRuntimeConfig } from "../runtime/config-validator.js";
|
|
6
|
+
import { validateOrThrow, SCHEMAS } from "./schema-validator.js";
|
|
7
|
+
|
|
8
|
+
const PATH_KEYS = new Set(["workspaceRoot", "soulRef", "purposeRef", "userRef"]);
|
|
9
|
+
|
|
10
|
+
async function loadDirectoryAsMap(dirPath, projectRoot, keyField = "id", { schema } = {}) {
|
|
11
|
+
const files = (await listFilesRecursive(dirPath)).filter((filePath) => filePath.endsWith(".toml"));
|
|
12
|
+
const entries = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
const parsed = resolveConfigPaths(normalizeKeys(parseToml(await readText(filePath, ""))), {
|
|
16
|
+
projectRoot
|
|
17
|
+
});
|
|
18
|
+
if (schema) {
|
|
19
|
+
validateOrThrow(parsed, schema, filePath);
|
|
20
|
+
}
|
|
21
|
+
const id = parsed[keyField] || path.basename(filePath, ".toml");
|
|
22
|
+
entries.push([id, { ...parsed, __filePath: filePath }]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Object.fromEntries(entries);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeKeys(value) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value.map((item) => normalizeKeys(item));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!value || typeof value !== "object") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalized = {};
|
|
38
|
+
for (const [key, child] of Object.entries(value)) {
|
|
39
|
+
const normalizedKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
40
|
+
normalized[normalizedKey] = normalizeKeys(child);
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function expandEnvRefs(value) {
|
|
46
|
+
return String(value || "").replace(/\$\{([^}]+)\}|\$([A-Z0-9_]+)/gi, (_match, braced, simple) => {
|
|
47
|
+
const envName = braced || simple;
|
|
48
|
+
return process.env[envName] || "";
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveEnvValue(value) {
|
|
53
|
+
if (typeof value === "string" && value.startsWith("env:")) {
|
|
54
|
+
return process.env[value.slice(4)] || "";
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolvePathLike(value) {
|
|
60
|
+
if (typeof value !== "string") return value;
|
|
61
|
+
const expanded = expandEnvRefs(value);
|
|
62
|
+
if (!expanded) return expanded;
|
|
63
|
+
if (expanded === "~") return homedir();
|
|
64
|
+
if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
|
|
65
|
+
return path.normalize(homedir() + expanded.slice(1));
|
|
66
|
+
}
|
|
67
|
+
if (path.isAbsolute(expanded)) return path.normalize(expanded);
|
|
68
|
+
return expanded;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveConfigPaths(value, { projectRoot, parentKey = null } = {}) {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.map((item) => resolveConfigPaths(item, { projectRoot, parentKey }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
if (PATH_KEYS.has(parentKey)) {
|
|
78
|
+
const resolvedPath = resolvePathLike(value);
|
|
79
|
+
if (!resolvedPath || path.isAbsolute(resolvedPath)) {
|
|
80
|
+
return resolvedPath;
|
|
81
|
+
}
|
|
82
|
+
return path.resolve(projectRoot, resolvedPath);
|
|
83
|
+
}
|
|
84
|
+
return expandEnvRefs(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!value || typeof value !== "object") {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolved = {};
|
|
92
|
+
for (const [key, child] of Object.entries(value)) {
|
|
93
|
+
resolved[key] = resolveConfigPaths(child, { projectRoot, parentKey: key });
|
|
94
|
+
}
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class ConfigLoader {
|
|
99
|
+
constructor({ rootDir }) {
|
|
100
|
+
this.rootDir = rootDir;
|
|
101
|
+
this.projectRoot = path.dirname(rootDir);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async loadRouter() {
|
|
105
|
+
const filePath = path.join(this.rootDir, "router.toml");
|
|
106
|
+
const parsed = parseToml(await readText(filePath, ""));
|
|
107
|
+
const lanes = {};
|
|
108
|
+
for (const [laneName, rawValue] of Object.entries(parsed.lanes || {})) {
|
|
109
|
+
const value = normalizeKeys(rawValue);
|
|
110
|
+
lanes[laneName] = {
|
|
111
|
+
primary: value.primary,
|
|
112
|
+
fallbackModels: value.fallbackModels || [],
|
|
113
|
+
fallback: value.fallback,
|
|
114
|
+
manualBump: value.manualBump
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
validateOrThrow(lanes, SCHEMAS.router, filePath);
|
|
118
|
+
return lanes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async loadProviders() {
|
|
122
|
+
return loadDirectoryAsMap(path.join(this.rootDir, "providers"), this.projectRoot, "id", { schema: SCHEMAS.provider });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async loadEmbeddings() {
|
|
126
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "embeddings.toml"), "")));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async loadRuntimeSettings() {
|
|
130
|
+
const filePath = path.join(this.rootDir, "runtime.toml");
|
|
131
|
+
const parsed = normalizeKeys(parseToml(await readText(filePath, "")));
|
|
132
|
+
validateOrThrow(parsed, SCHEMAS.runtime, filePath);
|
|
133
|
+
const configuredMaxJobsPerTick = parsed.concurrency?.maxJobsPerTick ?? parsed.concurrency?.maxConcurrentJobs;
|
|
134
|
+
return {
|
|
135
|
+
safety: parsed.safety || {},
|
|
136
|
+
concurrency: {
|
|
137
|
+
...parsed.concurrency,
|
|
138
|
+
maxJobsPerTick: configuredMaxJobsPerTick ?? 2
|
|
139
|
+
},
|
|
140
|
+
retention: {
|
|
141
|
+
runs: parsed.retention?.runs || {},
|
|
142
|
+
notifications: parsed.retention?.notifications || {},
|
|
143
|
+
deliveries: parsed.retention?.deliveries || {},
|
|
144
|
+
transportInbox: parsed.retention?.transportInbox || {}
|
|
145
|
+
},
|
|
146
|
+
retrieval: {
|
|
147
|
+
lexicalWeight: parsed.retrieval?.lexicalWeight ?? 0.36,
|
|
148
|
+
embeddingWeight: parsed.retrieval?.embeddingWeight ?? 0.3,
|
|
149
|
+
recencyWeight: parsed.retrieval?.recencyWeight ?? 0.14,
|
|
150
|
+
salienceWeight: parsed.retrieval?.salienceWeight ?? 0.14,
|
|
151
|
+
typeWeight: parsed.retrieval?.typeWeight ?? 0.06,
|
|
152
|
+
semanticRescueBonus: parsed.retrieval?.semanticRescueBonus ?? 0.06,
|
|
153
|
+
shadowSnapshotPenalty: parsed.retrieval?.shadowSnapshotPenalty ?? 0.12
|
|
154
|
+
},
|
|
155
|
+
memoryLocks: {
|
|
156
|
+
ttlMs: parsed.memoryLocks?.ttlMs ?? 15000,
|
|
157
|
+
retryDelayMs: parsed.memoryLocks?.retryDelayMs ?? 25,
|
|
158
|
+
maxRetries: parsed.memoryLocks?.maxRetries ?? 40
|
|
159
|
+
},
|
|
160
|
+
handoffs: {
|
|
161
|
+
pendingTimeoutMinutes: parsed.handoffs?.pendingTimeoutMinutes ?? 120,
|
|
162
|
+
escalateOnExpiry: parsed.handoffs?.escalateOnExpiry ?? true,
|
|
163
|
+
escalationDeliveryProfile: parsed.handoffs?.escalationDeliveryProfile || null
|
|
164
|
+
},
|
|
165
|
+
followUps: {
|
|
166
|
+
pendingTimeoutMinutes: parsed.followUps?.pendingTimeoutMinutes ?? 120,
|
|
167
|
+
escalateOnExpiry: parsed.followUps?.escalateOnExpiry ?? true,
|
|
168
|
+
escalationDeliveryProfile: parsed.followUps?.escalationDeliveryProfile || null,
|
|
169
|
+
completionTtlMultiplier: parsed.followUps?.completionTtlMultiplier ?? 2
|
|
170
|
+
},
|
|
171
|
+
yields: {
|
|
172
|
+
enabled: parsed.yields?.enabled ?? true,
|
|
173
|
+
defaultTargetSurface: parsed.yields?.defaultTargetSurface || "operator_review"
|
|
174
|
+
},
|
|
175
|
+
delivery: {
|
|
176
|
+
preventResendOnUncertain: parsed.delivery?.preventResendOnUncertain ?? true,
|
|
177
|
+
retryOnFailure: parsed.delivery?.retryOnFailure ?? false
|
|
178
|
+
},
|
|
179
|
+
maintenance: {
|
|
180
|
+
walCheckpointThresholdBytes: parsed.maintenance?.walCheckpointThresholdBytes ?? 64 * 1024 * 1024,
|
|
181
|
+
pruneOnTick: parsed.maintenance?.pruneOnTick ?? true,
|
|
182
|
+
sweepPendingHandoffsOnTick: parsed.maintenance?.sweepPendingHandoffsOnTick ?? true,
|
|
183
|
+
sweepPendingFollowUpsOnTick: parsed.maintenance?.sweepPendingFollowUpsOnTick ?? true
|
|
184
|
+
},
|
|
185
|
+
network: {
|
|
186
|
+
dnsResultOrder: parsed.network?.dnsResultOrder || "system",
|
|
187
|
+
connectTimeoutMs: parsed.network?.connectTimeoutMs ?? 10000,
|
|
188
|
+
readTimeoutMs: parsed.network?.readTimeoutMs ?? 30000,
|
|
189
|
+
retryBudget: parsed.network?.retryBudget ?? 1,
|
|
190
|
+
circuitBreakerThreshold: parsed.network?.circuitBreakerThreshold ?? 3
|
|
191
|
+
},
|
|
192
|
+
bootstrapCache: {
|
|
193
|
+
enabled: parsed.bootstrapCache?.enabled ?? true,
|
|
194
|
+
identityTtlMs: parsed.bootstrapCache?.identityTtlMs ?? 300000
|
|
195
|
+
},
|
|
196
|
+
reportFallback: {
|
|
197
|
+
enabled: parsed.reportFallback?.enabled ?? false,
|
|
198
|
+
lane: parsed.reportFallback?.lane || "report_fallback_lowcost",
|
|
199
|
+
allowedJobIds: parsed.reportFallback?.allowedJobIds || ["workspace-health"],
|
|
200
|
+
allowedFailureClasses: parsed.reportFallback?.allowedFailureClasses || ["timeout", "provider_loading"]
|
|
201
|
+
},
|
|
202
|
+
shutdown: {
|
|
203
|
+
drainTimeoutMs: parsed.shutdown?.drainTimeoutMs ?? 15000,
|
|
204
|
+
transportShutdownTimeoutMs: parsed.shutdown?.transportShutdownTimeoutMs ?? 5000
|
|
205
|
+
},
|
|
206
|
+
circuitBreaker: {
|
|
207
|
+
failureThreshold: parsed.circuitBreaker?.failureThreshold ?? 5,
|
|
208
|
+
resetTimeoutSeconds: parsed.circuitBreaker?.resetTimeoutSeconds ?? 30,
|
|
209
|
+
halfOpenMaxProbes: parsed.circuitBreaker?.halfOpenMaxProbes ?? 1,
|
|
210
|
+
transientCodes: parsed.circuitBreaker?.transientCodes ?? [408, 429, 500, 502, 503, 504]
|
|
211
|
+
},
|
|
212
|
+
extensions: {
|
|
213
|
+
implicitWorkspaceAutoload: parsed.extensions?.implicitWorkspaceAutoload ?? false,
|
|
214
|
+
requireExplicitTrust: parsed.extensions?.requireExplicitTrust ?? true,
|
|
215
|
+
trustedRoots: parsed.extensions?.trustedRoots || []
|
|
216
|
+
},
|
|
217
|
+
telegram: {
|
|
218
|
+
botTokenEnv: parsed.telegram?.botTokenEnv || "NEMORIS_TELEGRAM_BOT_TOKEN",
|
|
219
|
+
pollingMode: parsed.telegram?.pollingMode ?? false,
|
|
220
|
+
webhookUrl: parsed.telegram?.webhookUrl || "",
|
|
221
|
+
operatorChatId: parsed.telegram?.operatorChatId || "",
|
|
222
|
+
authorizedChatIds: parsed.telegram?.authorizedChatIds || [],
|
|
223
|
+
defaultAgent: parsed.telegram?.defaultAgent || "nemo",
|
|
224
|
+
},
|
|
225
|
+
slack: {
|
|
226
|
+
enabled: parsed.slack?.enabled ?? false,
|
|
227
|
+
botToken: resolveEnvValue(parsed.slack?.botToken || ""),
|
|
228
|
+
signingSecret: resolveEnvValue(parsed.slack?.signingSecret || ""),
|
|
229
|
+
eventsPath: parsed.slack?.eventsPath || "/slack/events",
|
|
230
|
+
slashPath: parsed.slack?.slashPath || "/slack/slash",
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async loadDelivery() {
|
|
236
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "delivery.toml"), "")));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async loadTaskRouter() {
|
|
240
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "task-router.toml"), "")));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async loadOutputContracts() {
|
|
244
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "output-contracts.toml"), "")));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async loadImprovementTargets() {
|
|
248
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "improvement-targets.toml"), "")));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async loadPeers() {
|
|
252
|
+
return normalizeKeys(parseToml(await readText(path.join(this.rootDir, "peers.toml"), "")));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async loadAgents() {
|
|
256
|
+
return loadDirectoryAsMap(path.join(this.rootDir, "agents"), this.projectRoot, "id", { schema: SCHEMAS.agent });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async loadJobs() {
|
|
260
|
+
return loadDirectoryAsMap(path.join(this.rootDir, "jobs"), this.projectRoot, "id", { schema: SCHEMAS.job });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async loadPolicies() {
|
|
264
|
+
return loadDirectoryAsMap(path.join(this.rootDir, "policies"), this.projectRoot);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async loadMcp() {
|
|
268
|
+
const filePath = path.join(this.rootDir, "mcp.toml");
|
|
269
|
+
try {
|
|
270
|
+
const raw = await readText(filePath, "");
|
|
271
|
+
if (!raw.trim()) return { servers: {} };
|
|
272
|
+
const parsed = normalizeKeys(parseToml(raw));
|
|
273
|
+
const servers = {};
|
|
274
|
+
for (const [id, serverConfig] of Object.entries(parsed.servers || {})) {
|
|
275
|
+
if (serverConfig.enabled === false) continue;
|
|
276
|
+
servers[id] = {
|
|
277
|
+
command: serverConfig.command,
|
|
278
|
+
args: serverConfig.args || [],
|
|
279
|
+
env: serverConfig.env || {},
|
|
280
|
+
timeout: serverConfig.timeout || 30000,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return { servers };
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (err.code === "ENOENT" || err.message?.includes("ENOENT")) return { servers: {} };
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async loadAll(options = {}) {
|
|
291
|
+
const [router, providers, agents, jobs, policies, runtime, taskRouter, delivery, peers, outputContracts, improvementTargets, mcp] = await Promise.all([
|
|
292
|
+
this.loadRouter(),
|
|
293
|
+
this.loadProviders(),
|
|
294
|
+
this.loadAgents(),
|
|
295
|
+
this.loadJobs(),
|
|
296
|
+
this.loadPolicies(),
|
|
297
|
+
this.loadRuntimeSettings(),
|
|
298
|
+
this.loadTaskRouter(),
|
|
299
|
+
this.loadDelivery(),
|
|
300
|
+
this.loadPeers(),
|
|
301
|
+
this.loadOutputContracts(),
|
|
302
|
+
this.loadImprovementTargets(),
|
|
303
|
+
this.loadMcp().catch((err) => {
|
|
304
|
+
if (err.code !== "ENOENT" && !err.message?.includes("ENOENT")) {
|
|
305
|
+
console.warn(`[config] mcp.toml parse error: ${err.message}`);
|
|
306
|
+
}
|
|
307
|
+
return { servers: {} };
|
|
308
|
+
})
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
const config = {
|
|
312
|
+
router,
|
|
313
|
+
providers,
|
|
314
|
+
agents,
|
|
315
|
+
jobs,
|
|
316
|
+
policies,
|
|
317
|
+
runtime,
|
|
318
|
+
taskRouter,
|
|
319
|
+
delivery,
|
|
320
|
+
peers,
|
|
321
|
+
outputContracts,
|
|
322
|
+
improvementTargets: improvementTargets.targets || {},
|
|
323
|
+
mcp,
|
|
324
|
+
embeddings: await this.loadEmbeddings()
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (!options?.skipValidation) {
|
|
328
|
+
await validateRuntimeConfig(config);
|
|
329
|
+
}
|
|
330
|
+
return config;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOML config schema validation — validates parsed config objects against
|
|
3
|
+
* expected schemas at load time so that missing/malformed fields fail loudly
|
|
4
|
+
* instead of silently degrading at runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
|
|
8
|
+
|
|
9
|
+
// ── Schema definitions ──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const AGENT_SCHEMA = {
|
|
12
|
+
label: "agent",
|
|
13
|
+
required: {
|
|
14
|
+
id: "string",
|
|
15
|
+
primaryLane: "string",
|
|
16
|
+
memoryPolicy: "string",
|
|
17
|
+
toolPolicy: "string"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const JOB_SCHEMA = {
|
|
22
|
+
label: "job",
|
|
23
|
+
required: {
|
|
24
|
+
id: "string",
|
|
25
|
+
modelLane: "string"
|
|
26
|
+
},
|
|
27
|
+
atLeastOne: [["schedule", "trigger"]]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROVIDER_SCHEMA = {
|
|
31
|
+
label: "provider",
|
|
32
|
+
required: {
|
|
33
|
+
id: "string"
|
|
34
|
+
},
|
|
35
|
+
atLeastOne: [["adapter", "type"]]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RUNTIME_SCHEMA = {
|
|
39
|
+
label: "runtime",
|
|
40
|
+
requiredSections: {
|
|
41
|
+
safety: {
|
|
42
|
+
contextTokens: "number"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ROUTER_SCHEMA = {
|
|
48
|
+
label: "router",
|
|
49
|
+
minLanes: 1
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const SCHEMAS = {
|
|
53
|
+
agent: AGENT_SCHEMA,
|
|
54
|
+
job: JOB_SCHEMA,
|
|
55
|
+
provider: PROVIDER_SCHEMA,
|
|
56
|
+
runtime: RUNTIME_SCHEMA,
|
|
57
|
+
router: ROUTER_SCHEMA
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Validation engine ───────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export class ConfigSchemaError extends Error {
|
|
63
|
+
constructor(message, errors) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "ConfigSchemaError";
|
|
66
|
+
this.validationErrors = errors;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function typeLabel(value) {
|
|
71
|
+
if (value === null || value === undefined) return "missing";
|
|
72
|
+
if (Array.isArray(value)) return "array";
|
|
73
|
+
return typeof value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkType(value, expectedType) {
|
|
77
|
+
if (value === null || value === undefined) return false;
|
|
78
|
+
if (expectedType === "array") return Array.isArray(value);
|
|
79
|
+
return typeof value === expectedType;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a parsed config object against a schema definition.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} config – the parsed (and key-normalised) config object
|
|
86
|
+
* @param {object} schema – one of the SCHEMAS entries
|
|
87
|
+
* @param {string} filePath – file path for error messages
|
|
88
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
89
|
+
*/
|
|
90
|
+
export function validateConfig(config, schema, filePath) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const ctx = filePath || schema.label || "config";
|
|
93
|
+
|
|
94
|
+
if (!config || typeof config !== "object") {
|
|
95
|
+
errors.push(`${ctx}: config is ${typeLabel(config)}, expected an object`);
|
|
96
|
+
return { ok: false, errors };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check required fields
|
|
100
|
+
if (schema.required) {
|
|
101
|
+
for (const [field, expectedType] of Object.entries(schema.required)) {
|
|
102
|
+
if (!(field in config) || config[field] === undefined || config[field] === null || config[field] === "") {
|
|
103
|
+
errors.push(`${ctx}: missing required field "${field}"`);
|
|
104
|
+
} else if (!checkType(config[field], expectedType)) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`${ctx}: field "${field}" should be ${expectedType}, got ${typeLabel(config[field])}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check at-least-one groups (e.g. schedule OR trigger)
|
|
113
|
+
if (schema.atLeastOne) {
|
|
114
|
+
for (const group of schema.atLeastOne) {
|
|
115
|
+
const present = group.filter((field) => field in config && config[field] !== undefined && config[field] !== null && config[field] !== "");
|
|
116
|
+
if (present.length === 0) {
|
|
117
|
+
errors.push(`${ctx}: must have at least one of: ${group.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check required sections (nested objects with typed sub-fields)
|
|
123
|
+
if (schema.requiredSections) {
|
|
124
|
+
for (const [section, subFields] of Object.entries(schema.requiredSections)) {
|
|
125
|
+
if (!(section in config) || !config[section] || typeof config[section] !== "object") {
|
|
126
|
+
errors.push(`${ctx}: missing required section [${section}]`);
|
|
127
|
+
} else {
|
|
128
|
+
for (const [subField, expectedType] of Object.entries(subFields)) {
|
|
129
|
+
const value = config[section][subField];
|
|
130
|
+
if (value === undefined || value === null) {
|
|
131
|
+
errors.push(`${ctx}: missing required field "${section}.${subField}"`);
|
|
132
|
+
} else if (!checkType(value, expectedType)) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`${ctx}: field "${section}.${subField}" should be ${expectedType}, got ${typeLabel(value)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Router-specific: must have at least N lane definitions
|
|
143
|
+
if (schema.minLanes !== undefined) {
|
|
144
|
+
const laneCount = Object.keys(config).length;
|
|
145
|
+
if (laneCount < schema.minLanes) {
|
|
146
|
+
errors.push(`${ctx}: must define at least ${schema.minLanes} lane(s), found ${laneCount}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { ok: errors.length === 0, errors };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate and throw if invalid — convenience wrapper used by the config loader.
|
|
155
|
+
*/
|
|
156
|
+
export function validateOrThrow(config, schema, filePath) {
|
|
157
|
+
const result = validateConfig(config, schema, filePath);
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
const runtimeError = new RuntimeError(
|
|
160
|
+
`Config schema validation failed for ${filePath || schema.label}`,
|
|
161
|
+
{ category: "config", context: { filePath, schema: schema.label, errorCount: result.errors.length }, recoverable: false }
|
|
162
|
+
);
|
|
163
|
+
const formatted = formatRuntimeError(runtimeError, { details: result.errors.join("; ") });
|
|
164
|
+
throw new ConfigSchemaError(formatted, result.errors);
|
|
165
|
+
}
|
|
166
|
+
return config;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate all config sections in a full loadAll() result.
|
|
171
|
+
* Returns { ok, errors[] } for CLI / test consumption.
|
|
172
|
+
*/
|
|
173
|
+
export function validateAllConfigs(config, rootDir) {
|
|
174
|
+
const allErrors = [];
|
|
175
|
+
|
|
176
|
+
// Agents
|
|
177
|
+
for (const [id, agent] of Object.entries(config.agents || {})) {
|
|
178
|
+
const filePath = agent.__filePath || `${rootDir}/agents/${id}.toml`;
|
|
179
|
+
const result = validateConfig(agent, AGENT_SCHEMA, filePath);
|
|
180
|
+
allErrors.push(...result.errors);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Jobs
|
|
184
|
+
for (const [id, job] of Object.entries(config.jobs || {})) {
|
|
185
|
+
const filePath = job.__filePath || `${rootDir}/jobs/${id}.toml`;
|
|
186
|
+
const result = validateConfig(job, JOB_SCHEMA, filePath);
|
|
187
|
+
allErrors.push(...result.errors);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Providers
|
|
191
|
+
for (const [id, provider] of Object.entries(config.providers || {})) {
|
|
192
|
+
const filePath = provider.__filePath || `${rootDir}/providers/${id}.toml`;
|
|
193
|
+
const result = validateConfig(provider, PROVIDER_SCHEMA, filePath);
|
|
194
|
+
allErrors.push(...result.errors);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Runtime
|
|
198
|
+
if (config.runtime) {
|
|
199
|
+
const result = validateConfig(config.runtime, RUNTIME_SCHEMA, `${rootDir}/runtime.toml`);
|
|
200
|
+
allErrors.push(...result.errors);
|
|
201
|
+
} else {
|
|
202
|
+
allErrors.push(`${rootDir}/runtime.toml: runtime config is missing entirely`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Router
|
|
206
|
+
if (config.router) {
|
|
207
|
+
const result = validateConfig(config.router, ROUTER_SCHEMA, `${rootDir}/router.toml`);
|
|
208
|
+
allErrors.push(...result.errors);
|
|
209
|
+
} else {
|
|
210
|
+
allErrors.push(`${rootDir}/router.toml: router config is missing entirely`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { ok: allErrors.length === 0, errors: allErrors };
|
|
214
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory that creates an action executor wired to real system components.
|
|
3
|
+
* Returns an async function: (actionName, context) => boolean (success).
|
|
4
|
+
*/
|
|
5
|
+
export function createActionHandler({ getBreaker, mcpConsumer, stateStore, telegramSendFn, lastFailedMessage }) {
|
|
6
|
+
const handlers = {
|
|
7
|
+
async circuit_break(ctx) {
|
|
8
|
+
const breaker = getBreaker(ctx.provider);
|
|
9
|
+
if (breaker) breaker.recordFailure(ctx.code || 500);
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
async retry_then_break(ctx) {
|
|
13
|
+
// Retry once, then circuit break
|
|
14
|
+
const breaker = getBreaker(ctx.provider);
|
|
15
|
+
if (breaker) breaker.recordFailure(ctx.code || 408);
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
async restart(ctx) {
|
|
19
|
+
if (!mcpConsumer) return false;
|
|
20
|
+
await mcpConsumer.ensureRunning(ctx.server);
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
async kill_restart(ctx) {
|
|
24
|
+
if (!mcpConsumer) return false;
|
|
25
|
+
await mcpConsumer.shutdown(ctx.server);
|
|
26
|
+
await mcpConsumer.ensureRunning(ctx.server);
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
async compact() {
|
|
30
|
+
// Trigger is handled by existing maintenance cycle — just return true
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
async rebuild_index() {
|
|
34
|
+
return true; // Existing embedding rebuild runs in maintenance
|
|
35
|
+
},
|
|
36
|
+
async reap() {
|
|
37
|
+
try {
|
|
38
|
+
const { reapOrphanedJobs } = await import("../runtime/orphan-reaper.js");
|
|
39
|
+
reapOrphanedJobs(stateStore);
|
|
40
|
+
} catch {
|
|
41
|
+
// stateStore may be a stub in tests or unavailable — best-effort
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
async retry_backoff(ctx) {
|
|
46
|
+
if (!telegramSendFn) return false;
|
|
47
|
+
const delays = [1000, 2000, 4000]; // 3 retries with exponential backoff
|
|
48
|
+
for (const delay of delays) {
|
|
49
|
+
await new Promise(r => setTimeout(r, delay));
|
|
50
|
+
const result = await telegramSendFn(lastFailedMessage || "Retry");
|
|
51
|
+
if (result?.ok) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
},
|
|
55
|
+
async resend(ctx) {
|
|
56
|
+
if (!telegramSendFn || !lastFailedMessage) return false;
|
|
57
|
+
const result = await telegramSendFn(lastFailedMessage);
|
|
58
|
+
return !!result?.ok;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return async (actionName, context) => {
|
|
63
|
+
const handler = handlers[actionName];
|
|
64
|
+
if (!handler) return false;
|
|
65
|
+
try {
|
|
66
|
+
return await handler(context || {});
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|