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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle /approve <id> — routes to repair_log (held updates) or rule_staging (rule approvals).
|
|
3
|
+
* Checks repair_log first, then rule_staging.
|
|
4
|
+
* Returns { found, table, type, needsNurseJob } so the caller can queue Nurse work.
|
|
5
|
+
*/
|
|
6
|
+
export function handleApproveCommand(id, repairLog, ruleStaging) {
|
|
7
|
+
const repairEntry = repairLog.getById(id);
|
|
8
|
+
if (repairEntry && repairEntry.result === "held") {
|
|
9
|
+
return { found: true, table: "repair_log", type: "held_update", entry: repairEntry, needsNurseJob: true };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ruleEntry = ruleStaging.getById(id);
|
|
13
|
+
if (ruleEntry && ruleEntry.status === "pending" && ruleEntry.action_class === "approval_required") {
|
|
14
|
+
const approved = ruleStaging.approve(id);
|
|
15
|
+
return { found: true, table: "rule_staging", type: "rule_approval", approved, entry: ruleEntry, needsNurseJob: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { found: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle /clear-repairs — marks unresolved repairs as acknowledged.
|
|
23
|
+
* Writes a "cleared" child row for each unresolved entry.
|
|
24
|
+
*/
|
|
25
|
+
export function handleClearRepairsCommand(repairLog) {
|
|
26
|
+
const unresolved = repairLog.getRecent(100).filter(
|
|
27
|
+
e => e.result === "unresolved" || (e.result === "escalated" && e.escalated === 1)
|
|
28
|
+
);
|
|
29
|
+
for (const entry of unresolved) {
|
|
30
|
+
repairLog.write({
|
|
31
|
+
source: "user", type: entry.type, action: "manual_clear",
|
|
32
|
+
result: "resolved", severity: "silent", parent_id: entry.id,
|
|
33
|
+
diagnosis: "Manually cleared by user via /clear-repairs"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return `${unresolved.length} unresolved repair(s) cleared.`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handle /health — return system health summary.
|
|
41
|
+
*/
|
|
42
|
+
export function handleHealthCommand(repairLog) {
|
|
43
|
+
const recent = repairLog.getRecent(50);
|
|
44
|
+
const resolved = recent.filter(e => e.result === "resolved").length;
|
|
45
|
+
const unresolved = recent.filter(e => e.result === "unresolved" || e.result === "escalated").length;
|
|
46
|
+
const held = recent.filter(e => e.result === "held").length;
|
|
47
|
+
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(`Last 50 repairs: ${resolved} resolved, ${unresolved} unresolved, ${held} held`);
|
|
50
|
+
if (unresolved > 0) lines.push(`⚠ ${unresolved} unresolved issue(s) need attention`);
|
|
51
|
+
if (held > 0) lines.push(`📦 ${held} update(s) awaiting /approve`);
|
|
52
|
+
if (unresolved === 0 && held === 0) lines.push("All systems healthy.");
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function detectInstallType(projectRoot) {
|
|
5
|
+
return fs.existsSync(path.join(projectRoot, ".git")) ? "git" : "npm";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function classifySemver(current, next) {
|
|
9
|
+
const [cMajor, cMinor, cPatch] = current.split(".").map(Number);
|
|
10
|
+
const [nMajor, nMinor, nPatch] = next.split(".").map(Number);
|
|
11
|
+
if (nMajor > cMajor) return "major";
|
|
12
|
+
if (nMinor > cMinor) return "minor";
|
|
13
|
+
if (nPatch > cPatch) return "patch";
|
|
14
|
+
return "none";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldAutoApply(target, semverClass) {
|
|
18
|
+
if (semverClass === "none") return false;
|
|
19
|
+
if (target === "nemoris") {
|
|
20
|
+
return semverClass === "patch" || semverClass === "minor";
|
|
21
|
+
}
|
|
22
|
+
return semverClass === "patch";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function snapshotConfig(projectRoot) {
|
|
26
|
+
const configDir = path.join(projectRoot, "config");
|
|
27
|
+
const snapshot = {};
|
|
28
|
+
if (!fs.existsSync(configDir)) return snapshot;
|
|
29
|
+
for (const file of fs.readdirSync(configDir)) {
|
|
30
|
+
if (file.endsWith(".toml") || file.endsWith(".json")) {
|
|
31
|
+
snapshot[`config/${file}`] = fs.readFileSync(path.join(configDir, file), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return snapshot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function restoreConfig(projectRoot, snapshot) {
|
|
38
|
+
for (const [relPath, content] of Object.entries(snapshot)) {
|
|
39
|
+
const absPath = path.join(projectRoot, relPath);
|
|
40
|
+
fs.writeFileSync(absPath, content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseNpmOutdated(stdout) {
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(stdout);
|
|
47
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
48
|
+
name,
|
|
49
|
+
current: info.current,
|
|
50
|
+
latest: info.latest,
|
|
51
|
+
wanted: info.wanted,
|
|
52
|
+
target: "npm",
|
|
53
|
+
semverClass: classifySemver(info.current, info.latest)
|
|
54
|
+
}));
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildUpdatePlan(updates) {
|
|
61
|
+
const autoApply = [];
|
|
62
|
+
const hold = [];
|
|
63
|
+
for (const u of updates) {
|
|
64
|
+
if (shouldAutoApply(u.target, u.semverClass || classifySemver(u.current, u.latest))) {
|
|
65
|
+
autoApply.push(u);
|
|
66
|
+
} else {
|
|
67
|
+
hold.push(u);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { autoApply, hold };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runSmokeTest({ healthProbes, localModelAvailable, runTestJob }) {
|
|
74
|
+
const probeResults = await healthProbes();
|
|
75
|
+
const failures = probeResults.filter(r => r.status === "failure");
|
|
76
|
+
if (failures.length > 0) return { passed: false, reason: "probe_failure", failures };
|
|
77
|
+
|
|
78
|
+
// Only run per-lane test job if local model is available (never cloud)
|
|
79
|
+
if (localModelAvailable && runTestJob) {
|
|
80
|
+
const jobResult = await runTestJob({ model: "local" });
|
|
81
|
+
if (!jobResult.success) return { passed: false, reason: "test_job_failure" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { passed: true };
|
|
85
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
function nowIso() {
|
|
2
|
+
return new Date().toISOString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class JobRunner {
|
|
6
|
+
constructor({ memoryStore }) {
|
|
7
|
+
this.memoryStore = memoryStore;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async run(job, execute) {
|
|
11
|
+
const runId = `${job.id}:${Date.now()}`;
|
|
12
|
+
const agentId = job.agentId;
|
|
13
|
+
|
|
14
|
+
await this.memoryStore.appendEvent(agentId, {
|
|
15
|
+
title: `job:${job.id}:queued`,
|
|
16
|
+
content: `Run ${runId} queued`,
|
|
17
|
+
category: "job_state",
|
|
18
|
+
salience: 0.4
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await this.memoryStore.saveCheckpoint(agentId, "latest", {
|
|
22
|
+
objective: job.taskType,
|
|
23
|
+
status: "running",
|
|
24
|
+
nextActions: [],
|
|
25
|
+
blockers: [],
|
|
26
|
+
runId
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await execute({
|
|
31
|
+
runId,
|
|
32
|
+
budget: job.budget
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await this.memoryStore.writeSummary(agentId, {
|
|
36
|
+
title: `job:${job.id}:summary`,
|
|
37
|
+
summary: result.summary,
|
|
38
|
+
content: result.output,
|
|
39
|
+
category: "job_summary",
|
|
40
|
+
salience: 0.72
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await this.memoryStore.saveCheckpoint(agentId, "latest", {
|
|
44
|
+
objective: job.taskType,
|
|
45
|
+
status: "succeeded",
|
|
46
|
+
nextActions: result.nextActions || [],
|
|
47
|
+
blockers: []
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
runId,
|
|
52
|
+
status: "succeeded",
|
|
53
|
+
result
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
await this.memoryStore.appendEvent(agentId, {
|
|
57
|
+
title: `job:${job.id}:failed`,
|
|
58
|
+
content: error.message,
|
|
59
|
+
category: "job_failure",
|
|
60
|
+
salience: 0.78
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await this.memoryStore.saveCheckpoint(agentId, "latest", {
|
|
64
|
+
objective: job.taskType,
|
|
65
|
+
status: "failed",
|
|
66
|
+
nextActions: [],
|
|
67
|
+
blockers: [error.message],
|
|
68
|
+
failedAt: nowIso()
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
runId,
|
|
73
|
+
status: "failed",
|
|
74
|
+
error: error.message
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/mcp/consumer.js
|
|
2
|
+
import { expandEnvRefs } from "../config/loader.js";
|
|
3
|
+
import { McpClient } from "../tools/mcp-client.js";
|
|
4
|
+
|
|
5
|
+
export class McpConsumer {
|
|
6
|
+
#clients = new Map();
|
|
7
|
+
#schemaCache = new Map();
|
|
8
|
+
#config;
|
|
9
|
+
#clientFactory;
|
|
10
|
+
|
|
11
|
+
constructor({ config, clientFactory }) {
|
|
12
|
+
this.#config = config;
|
|
13
|
+
this.#clientFactory = clientFactory || ((serverConfig) => new McpClient(serverConfig));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async ensureRunning(serverId) {
|
|
17
|
+
const existing = this.#clients.get(serverId);
|
|
18
|
+
if (existing && existing.isAlive()) return existing;
|
|
19
|
+
|
|
20
|
+
if (existing) {
|
|
21
|
+
this.#schemaCache.delete(serverId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const serverConfig = this.#config.servers[serverId];
|
|
25
|
+
if (!serverConfig) throw new Error(`MCP server '${serverId}' not found in config`);
|
|
26
|
+
|
|
27
|
+
const resolvedEnv = {};
|
|
28
|
+
for (const [key, val] of Object.entries(serverConfig.env || {})) {
|
|
29
|
+
const raw = String(val);
|
|
30
|
+
const resolved = expandEnvRefs(raw);
|
|
31
|
+
const varMatch = raw.match(/\$\{([^}]+)\}/);
|
|
32
|
+
if (varMatch && !process.env[varMatch[1]]) {
|
|
33
|
+
console.warn(`[mcp:${serverId}] env var ${varMatch[1]} is not set`);
|
|
34
|
+
}
|
|
35
|
+
resolvedEnv[key] = resolved;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const client = this.#clientFactory({
|
|
39
|
+
command: serverConfig.command,
|
|
40
|
+
args: serverConfig.args || [],
|
|
41
|
+
env: resolvedEnv,
|
|
42
|
+
timeout: serverConfig.timeout || 30000,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await client.spawn();
|
|
46
|
+
this.#clients.set(serverId, client);
|
|
47
|
+
return client;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async callTool(serverId, toolName, args) {
|
|
51
|
+
let client;
|
|
52
|
+
try {
|
|
53
|
+
client = await this.ensureRunning(serverId);
|
|
54
|
+
return await client.callTool(toolName, args);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (client && !client.isAlive()) {
|
|
57
|
+
this.#clients.delete(serverId);
|
|
58
|
+
const retryClient = await this.ensureRunning(serverId);
|
|
59
|
+
return await retryClient.callTool(toolName, args);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async allSchemas() {
|
|
66
|
+
const schemas = [];
|
|
67
|
+
for (const serverId of Object.keys(this.#config.servers)) {
|
|
68
|
+
try {
|
|
69
|
+
const tools = await this.#getServerTools(serverId);
|
|
70
|
+
for (const tool of tools) {
|
|
71
|
+
schemas.push({
|
|
72
|
+
server: serverId,
|
|
73
|
+
name: `mcp:${serverId}.${tool.name}`,
|
|
74
|
+
description: tool.description || "",
|
|
75
|
+
inputSchema: tool.inputSchema || {},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn(`[mcp:${serverId}] failed to start: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return schemas;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cachedSchemas() {
|
|
86
|
+
const schemas = [];
|
|
87
|
+
for (const [serverId, tools] of this.#schemaCache) {
|
|
88
|
+
for (const tool of tools) {
|
|
89
|
+
schemas.push({
|
|
90
|
+
server: serverId,
|
|
91
|
+
name: `mcp:${serverId}.${tool.name}`,
|
|
92
|
+
description: tool.description || "",
|
|
93
|
+
inputSchema: tool.inputSchema || {},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return schemas;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async shutdown() {
|
|
101
|
+
for (const [, client] of this.#clients) {
|
|
102
|
+
try {
|
|
103
|
+
client.kill();
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort kill
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.#clients.clear();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
healthSummary() {
|
|
112
|
+
const summary = [];
|
|
113
|
+
for (const serverId of Object.keys(this.#config.servers)) {
|
|
114
|
+
const client = this.#clients.get(serverId);
|
|
115
|
+
summary.push({ id: serverId, alive: Boolean(client?.isAlive()) });
|
|
116
|
+
}
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async #getServerTools(serverId) {
|
|
121
|
+
const cached = this.#schemaCache.get(serverId);
|
|
122
|
+
if (cached) return cached;
|
|
123
|
+
|
|
124
|
+
const client = await this.ensureRunning(serverId);
|
|
125
|
+
const tools = await client.listTools();
|
|
126
|
+
this.#schemaCache.set(serverId, tools);
|
|
127
|
+
return tools;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active Recall — turn-aware memory scoring and tiered injection.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/superpowers/specs/2026-03-16-interaction-layer-design.md § Pillar 1
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── Scoring ─────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export function computeRecency(lastAccessedOrCreated, now = new Date()) {
|
|
10
|
+
const ts = new Date(lastAccessedOrCreated);
|
|
11
|
+
const daysSince = (now.getTime() - ts.getTime()) / (24 * 60 * 60 * 1000);
|
|
12
|
+
return 1 / (1 + daysSince * 0.1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function scoreCandidate({ similarity, topicRelevance, recency, confidence }) {
|
|
16
|
+
const hasTopicRelevance = topicRelevance > 0;
|
|
17
|
+
if (hasTopicRelevance) {
|
|
18
|
+
return (similarity * 0.5) + (topicRelevance * 0.3) + (recency * confidence * 0.2);
|
|
19
|
+
}
|
|
20
|
+
return (similarity * 0.7) + (recency * confidence * 0.3);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Tier classification ─────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const FACT_THRESHOLD = 0.85;
|
|
26
|
+
export const ASIDE_THRESHOLD = 0.70;
|
|
27
|
+
|
|
28
|
+
export function classifyTier(score) {
|
|
29
|
+
if (score > FACT_THRESHOLD) return "fact";
|
|
30
|
+
if (score >= ASIDE_THRESHOLD) return "aside";
|
|
31
|
+
return "silent";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Injection builder ───────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const CHARS_PER_TOKEN = 4;
|
|
37
|
+
|
|
38
|
+
export function buildInjection(candidates, { tokenBudget = 2000 } = {}) {
|
|
39
|
+
let remainingChars = tokenBudget * CHARS_PER_TOKEN;
|
|
40
|
+
const facts = [];
|
|
41
|
+
const asides = [];
|
|
42
|
+
const silent = [];
|
|
43
|
+
|
|
44
|
+
const sorted = [...candidates].sort((a, b) => b.score - a.score);
|
|
45
|
+
|
|
46
|
+
for (const c of sorted) {
|
|
47
|
+
if (c.tier === "silent") { silent.push(c); continue; }
|
|
48
|
+
if (c.tier === "fact") {
|
|
49
|
+
const len = c.content.length;
|
|
50
|
+
if (len <= remainingChars) {
|
|
51
|
+
facts.push(c);
|
|
52
|
+
remainingChars -= len;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const c of sorted) {
|
|
58
|
+
if (c.tier === "aside") {
|
|
59
|
+
const len = c.content.length;
|
|
60
|
+
if (len <= remainingChars) {
|
|
61
|
+
asides.push(c);
|
|
62
|
+
remainingChars -= len;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { facts, asides, silent };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Orchestrator ────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export async function activeRecall({ inboundMessage, memoryStore, tokenBudget = 2000, topicRelevanceFn = null, contextWindow = 0 }) {
|
|
73
|
+
const budget = contextWindow > 0 ? Math.min(tokenBudget, Math.floor(contextWindow * 0.08)) : tokenBudget;
|
|
74
|
+
|
|
75
|
+
// Stage 1: Candidate retrieval (top 20)
|
|
76
|
+
// queryRetrievalCandidates(query, options) — positional args
|
|
77
|
+
const raw = memoryStore.queryRetrievalCandidates
|
|
78
|
+
? memoryStore.queryRetrievalCandidates(inboundMessage, { limit: 20 })
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
// Unwrap nested shape: real store returns { item: { entry_id, content, ... }, embeddingSimilarity }
|
|
82
|
+
const candidates = raw.map((r) => ({
|
|
83
|
+
entryId: r.item?.entry_id || r.entryId,
|
|
84
|
+
content: r.item?.content || r.content,
|
|
85
|
+
summary: r.item?.summary || r.summary,
|
|
86
|
+
salience: r.item?.salience ?? r.salience ?? 0.5,
|
|
87
|
+
embeddingSimilarity: r.embeddingSimilarity ?? 0,
|
|
88
|
+
last_accessed: r.item?.last_accessed || r.last_accessed || null,
|
|
89
|
+
created_at: r.item?.timestamp || r.created_at,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// Stage 2: Turn-aware scoring
|
|
93
|
+
const now = new Date();
|
|
94
|
+
const scored = candidates.map((c) => {
|
|
95
|
+
const similarity = c.embeddingSimilarity || c.salience || 0.5;
|
|
96
|
+
const topicRelevance = topicRelevanceFn ? topicRelevanceFn(inboundMessage, c) : 0;
|
|
97
|
+
const recency = computeRecency(c.last_accessed || c.created_at || now.toISOString(), now);
|
|
98
|
+
const confidence = 0.75; // Default — declared memory (MEMORY.md) injected separately at 1.0
|
|
99
|
+
const score = scoreCandidate({ similarity, topicRelevance, recency, confidence });
|
|
100
|
+
const tier = classifyTier(score);
|
|
101
|
+
return { ...c, score, tier };
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Stage 3: Tiered injection
|
|
105
|
+
const injection = buildInjection(scored, { tokenBudget: budget });
|
|
106
|
+
|
|
107
|
+
// Stage 4: Access tracking (score >= 0.70)
|
|
108
|
+
const accessedIds = scored.filter((c) => c.score >= ASIDE_THRESHOLD).map((c) => c.entryId);
|
|
109
|
+
if (accessedIds.length && memoryStore.updateAccessTime) {
|
|
110
|
+
memoryStore.updateAccessTime(accessedIds);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return injection;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Write discipline ────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const DEDUP_THRESHOLD = 0.95;
|
|
119
|
+
const CONTRADICTION_LOW = 0.85;
|
|
120
|
+
|
|
121
|
+
export class WriteBuffer {
|
|
122
|
+
constructor({ commitFn, similarityFn, updateFn, existingEntries = [] }) {
|
|
123
|
+
this._pending = [];
|
|
124
|
+
this._commitFn = commitFn;
|
|
125
|
+
this._similarityFn = similarityFn;
|
|
126
|
+
this._updateFn = updateFn || (() => {});
|
|
127
|
+
this._existingEntries = existingEntries;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
add(entry) {
|
|
131
|
+
this._pending.push(entry);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
flush() {
|
|
135
|
+
let committed = 0;
|
|
136
|
+
let deduped = 0;
|
|
137
|
+
const contradictions = [];
|
|
138
|
+
|
|
139
|
+
for (const entry of this._pending) {
|
|
140
|
+
let dominated = false;
|
|
141
|
+
for (const existing of this._existingEntries) {
|
|
142
|
+
const sim = this._similarityFn(entry.content, existing.content);
|
|
143
|
+
if (sim > DEDUP_THRESHOLD) {
|
|
144
|
+
this._updateFn(existing.entryId, entry.content);
|
|
145
|
+
deduped++;
|
|
146
|
+
dominated = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (sim >= CONTRADICTION_LOW && sim <= DEDUP_THRESHOLD) {
|
|
150
|
+
contradictions.push({
|
|
151
|
+
existingId: existing.entryId,
|
|
152
|
+
existingContent: existing.content,
|
|
153
|
+
newContent: entry.content,
|
|
154
|
+
similarity: sim,
|
|
155
|
+
});
|
|
156
|
+
dominated = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!dominated) {
|
|
161
|
+
this._commitFn(entry);
|
|
162
|
+
committed++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this._pending = [];
|
|
167
|
+
return { committed, deduped, contradictions };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get pending() { return this._pending; }
|
|
171
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { FileMemoryBackend } from "./backends/file-backend.js";
|
|
2
|
+
import { QmdMemoryBackend } from "./backends/qmd-backend.js";
|
|
3
|
+
|
|
4
|
+
export class MemoryBackendManager {
|
|
5
|
+
constructor({ memoryStore, embeddingIndex, liveRoot }) {
|
|
6
|
+
this.fileBackend = new FileMemoryBackend({ memoryStore, embeddingIndex });
|
|
7
|
+
this.qmdBackend = new QmdMemoryBackend({ liveRoot });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
defaultBackend() {
|
|
11
|
+
return this.fileBackend;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async queryCombined(agentId, query, options = {}) {
|
|
15
|
+
const limit = options.limit ?? 8;
|
|
16
|
+
const workspaceRoot = options.workspaceRoot || null;
|
|
17
|
+
const backendOrder = options.backendOrder || ["file"];
|
|
18
|
+
const qmdSupplementLimit = options.qmdSupplementLimit ?? 2;
|
|
19
|
+
const fileQuery = options.fileQuery || query;
|
|
20
|
+
const qmdQuery = options.qmdQuery || query;
|
|
21
|
+
|
|
22
|
+
const fileResult = await this.fileBackend.query(agentId, fileQuery, {
|
|
23
|
+
limit,
|
|
24
|
+
now: options.now,
|
|
25
|
+
retrievalBlend: options.retrievalBlend
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let merged = [...fileResult.items];
|
|
29
|
+
let qmdResult = {
|
|
30
|
+
backend: "qmd",
|
|
31
|
+
available: false,
|
|
32
|
+
items: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (backendOrder.includes("qmd")) {
|
|
36
|
+
qmdResult = await this.qmdBackend.query(agentId, qmdQuery, {
|
|
37
|
+
workspaceRoot,
|
|
38
|
+
limit: qmdSupplementLimit
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const qmdItems = qmdResult.items.map((item, index) => ({
|
|
42
|
+
type: "external",
|
|
43
|
+
category: "qmd_match",
|
|
44
|
+
title: item.title,
|
|
45
|
+
content: item.snippet,
|
|
46
|
+
summary: item.filepath,
|
|
47
|
+
reason: "Matched through local QMD FTS retrieval.",
|
|
48
|
+
score: Number((0.35 - index * 0.03).toFixed(4)),
|
|
49
|
+
sourceBackend: "qmd",
|
|
50
|
+
candidateSource: "qmd",
|
|
51
|
+
lexicalScore: 0,
|
|
52
|
+
embeddingSimilarity: 0,
|
|
53
|
+
embeddingFreshness: "not_applicable",
|
|
54
|
+
retrievalSources: ["qmd"]
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
merged = [...merged, ...qmdItems];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
items: merged.slice(0, limit),
|
|
62
|
+
backends: {
|
|
63
|
+
file: {
|
|
64
|
+
available: fileResult.available,
|
|
65
|
+
totalCandidates: fileResult.totalCandidates ?? fileResult.items.length,
|
|
66
|
+
query: fileQuery
|
|
67
|
+
},
|
|
68
|
+
qmd: {
|
|
69
|
+
available: qmdResult.available,
|
|
70
|
+
totalCandidates: qmdResult.items.length,
|
|
71
|
+
stats: qmdResult.stats || null,
|
|
72
|
+
query: qmdQuery
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
retrieval: fileResult.retrieval || null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async inspect(agentId, workspaceRoot = null) {
|
|
80
|
+
const [file, qmd] = await Promise.all([
|
|
81
|
+
this.fileBackend.inspect(agentId),
|
|
82
|
+
this.qmdBackend.inspect(agentId, workspaceRoot)
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
defaultBackend: file.kind,
|
|
87
|
+
backends: [file, qmd]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async queryQmd(agentId, query, workspaceRoot = null, limit = 5) {
|
|
92
|
+
return this.qmdBackend.query(agentId, query, {
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
limit
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class FileMemoryBackend {
|
|
2
|
+
constructor({ memoryStore, embeddingIndex = null } = {}) {
|
|
3
|
+
this.kind = "file";
|
|
4
|
+
this.memoryStore = memoryStore;
|
|
5
|
+
this.embeddingIndex = embeddingIndex;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async query(agentId, query, options = {}) {
|
|
9
|
+
const result = await this.memoryStore.query(agentId, query, {
|
|
10
|
+
...options,
|
|
11
|
+
embeddingIndex: this.embeddingIndex
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
backend: this.kind,
|
|
15
|
+
available: true,
|
|
16
|
+
items: result.items.map((item) => ({
|
|
17
|
+
...item,
|
|
18
|
+
sourceBackend: this.kind
|
|
19
|
+
})),
|
|
20
|
+
totalCandidates: result.totalCandidates,
|
|
21
|
+
retrieval: result.retrieval || null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async listAll(agentId) {
|
|
26
|
+
return this.memoryStore.listAll(agentId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async inspect(agentId) {
|
|
30
|
+
const items = await this.memoryStore.listAll(agentId);
|
|
31
|
+
return {
|
|
32
|
+
kind: this.kind,
|
|
33
|
+
available: true,
|
|
34
|
+
itemCount: items.length,
|
|
35
|
+
embeddingIndexConfigured: Boolean(this.embeddingIndex)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|