amalfa 0.0.0-reserved ā 1.0.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/.biomeignore +19 -0
- package/:memory: +0 -0
- package/:memory:-shm +0 -0
- package/:memory:-wal +0 -0
- package/LICENSE +21 -0
- package/README.md +343 -13
- package/README.old.md +112 -0
- package/agents.config.json +11 -0
- package/amalfa.config.example.ts +100 -0
- package/biome.json +49 -0
- package/bun.lock +371 -0
- package/docs/AGENT_PROTOCOLS.md +28 -0
- package/docs/ARCHITECTURAL_OVERVIEW.md +123 -0
- package/docs/BENTO_BOXING_DEPRECATION.md +281 -0
- package/docs/Bun-SQLite.html +464 -0
- package/docs/COMMIT_GUIDELINES.md +367 -0
- package/docs/DEVELOPER_ONBOARDING.md +36 -0
- package/docs/Graph and Vector Database Best Practices.md +214 -0
- package/docs/PERFORMANCE_BASELINES.md +88 -0
- package/docs/REPOSITORY_CLEANUP_SUMMARY.md +261 -0
- package/docs/edge-generation-methods.md +57 -0
- package/docs/elevator-pitch.md +118 -0
- package/docs/graph-and-vector-database-playbook.html +480 -0
- package/docs/hardened-sqlite.md +85 -0
- package/docs/headless-knowledge-management.md +79 -0
- package/docs/john-kaye-flux-prompt.md +46 -0
- package/docs/keyboard-shortcuts.md +80 -0
- package/docs/opinion-proceed-pattern.md +29 -0
- package/docs/polyvis-nodes-edges-schema.md +77 -0
- package/docs/protocols/lab-protocol.md +30 -0
- package/docs/reaction-iquest-loop-coder.md +46 -0
- package/docs/services.md +60 -0
- package/docs/sqlite-wal-readonly-trap.md +228 -0
- package/docs/strategy/css-architecture.md +40 -0
- package/docs/test-document-cycle.md +83 -0
- package/docs/test_lifecycle_E2E.md +4 -0
- package/docs/the-bicameral-graph.md +83 -0
- package/docs/user-guide.md +70 -0
- package/docs/vision-helper.md +53 -0
- package/drizzle/0000_minor_iron_fist.sql +19 -0
- package/drizzle/meta/0000_snapshot.json +139 -0
- package/drizzle/meta/_journal.json +13 -0
- package/example_usage.ts +39 -0
- package/experiment.sh +35 -0
- package/hello +2 -0
- package/index.html +52 -0
- package/knowledge/excalibur.md +12 -0
- package/package.json +60 -15
- package/plans/experience-graph-integration.md +60 -0
- package/prompts/gemini-king-mode-prompt.md +46 -0
- package/public/docs/MCP_TOOLS.md +372 -0
- package/schemas/README.md +20 -0
- package/schemas/cda.schema.json +84 -0
- package/schemas/conceptual-lexicon.schema.json +75 -0
- package/scratchpads/dummy-debrief-boxed.md +39 -0
- package/scratchpads/dummy-debrief.md +27 -0
- package/scratchpads/scratchpad-design.md +50 -0
- package/scratchpads/scratchpad-scrolling.md +20 -0
- package/scratchpads/scratchpad-toc-disappearance.md +23 -0
- package/scratchpads/scratchpad-toc.md +28 -0
- package/scratchpads/test_gardener.md +7 -0
- package/src/EnlightenedTriad.ts +146 -0
- package/src/JIT_Triad.ts +137 -0
- package/src/cli.ts +318 -0
- package/src/config/constants.ts +7 -0
- package/src/config/defaults.ts +81 -0
- package/src/core/BentoNormalizer.ts +113 -0
- package/src/core/EdgeWeaver.ts +145 -0
- package/src/core/FractureLogic.ts +22 -0
- package/src/core/Harvester.ts +73 -0
- package/src/core/LLMClient.ts +93 -0
- package/src/core/LouvainGate.ts +67 -0
- package/src/core/MarkdownMasker.ts +49 -0
- package/src/core/README.md +11 -0
- package/src/core/SemanticMatcher.ts +89 -0
- package/src/core/SemanticWeaver.ts +96 -0
- package/src/core/TagEngine.ts +56 -0
- package/src/core/TimelineWeaver.ts +61 -0
- package/src/core/VectorEngine.ts +232 -0
- package/src/daemon/index.ts +221 -0
- package/src/data/experience/test_doc_1.md +2 -0
- package/src/data/experience/test_doc_2.md +2 -0
- package/src/db/schema.ts +46 -0
- package/src/demo-triad.ts +45 -0
- package/src/gardeners/AutoTagger.ts +116 -0
- package/src/gardeners/BaseGardener.ts +55 -0
- package/src/llm/EnlightenedProvider.ts +95 -0
- package/src/mcp/README.md +6 -0
- package/src/mcp/index.ts +341 -0
- package/src/pipeline/AmalfaIngestor.ts +262 -0
- package/src/pipeline/HarvesterPipeline.ts +101 -0
- package/src/pipeline/Ingestor.ts +555 -0
- package/src/pipeline/README.md +7 -0
- package/src/pipeline/SemanticHarvester.ts +222 -0
- package/src/resonance/DatabaseFactory.ts +100 -0
- package/src/resonance/README.md +148 -0
- package/src/resonance/cli/README.md +7 -0
- package/src/resonance/cli/ingest.ts +41 -0
- package/src/resonance/cli/migrate.ts +54 -0
- package/src/resonance/config.ts +40 -0
- package/src/resonance/daemon.ts +236 -0
- package/src/resonance/db.ts +422 -0
- package/src/resonance/pipeline/README.md +7 -0
- package/src/resonance/pipeline/extract.ts +89 -0
- package/src/resonance/pipeline/transform_docs.ts +60 -0
- package/src/resonance/schema.ts +138 -0
- package/src/resonance/services/embedder.ts +131 -0
- package/src/resonance/services/simpleTokenizer.ts +119 -0
- package/src/resonance/services/stats.ts +327 -0
- package/src/resonance/services/tokenizer.ts +159 -0
- package/src/resonance/transform/cda.ts +393 -0
- package/src/resonance/types/enriched-cda.ts +112 -0
- package/src/services/README.md +56 -0
- package/src/services/llama.ts +59 -0
- package/src/services/llamauv.ts +56 -0
- package/src/services/olmo3.ts +58 -0
- package/src/services/phi.ts +52 -0
- package/src/types/artifact.ts +12 -0
- package/src/utils/EnvironmentVerifier.ts +67 -0
- package/src/utils/Logger.ts +21 -0
- package/src/utils/ServiceLifecycle.ts +207 -0
- package/src/utils/ZombieDefense.ts +244 -0
- package/src/utils/validator.ts +264 -0
- package/substack/substack-playbook-1.md +95 -0
- package/substack/substack-playbook-2.md +78 -0
- package/tasks/ui-investigation.md +26 -0
- package/test-db +0 -0
- package/test-db-shm +0 -0
- package/test-db-wal +0 -0
- package/tests/canary/verify_pinch_check.ts +44 -0
- package/tests/fixtures/ingest_test.md +12 -0
- package/tests/fixtures/ingest_test_boxed.md +13 -0
- package/tests/fixtures/safety_test.md +45 -0
- package/tests/fixtures/safety_test_boxed.md +49 -0
- package/tests/fixtures/tagged_output.md +49 -0
- package/tests/fixtures/tagged_test.md +49 -0
- package/tests/mcp-server-settings.json +8 -0
- package/tsconfig.json +46 -0
- package/verify-embedder.ts +54 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ServiceLifecycle } from "../utils/ServiceLifecycle";
|
|
3
|
+
|
|
4
|
+
// --- Configuration ---
|
|
5
|
+
const BASE_DIR = join(import.meta.dir, "../../experiments/enlightenment");
|
|
6
|
+
const BIN_PATH = join(BASE_DIR, "llama.cpp/build/bin/llama-server");
|
|
7
|
+
const MODEL_PATH = join(BASE_DIR, "vectors/Olmo-3-7B-Think-Q4_K_M.gguf");
|
|
8
|
+
|
|
9
|
+
const PORT = 8084;
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0] || "serve";
|
|
13
|
+
|
|
14
|
+
// --- Service Lifecycle ---
|
|
15
|
+
|
|
16
|
+
const lifecycle = new ServiceLifecycle({
|
|
17
|
+
name: "Olmo-3",
|
|
18
|
+
pidFile: ".olmo3.pid",
|
|
19
|
+
logFile: ".olmo3.log",
|
|
20
|
+
entryPoint: "src/services/olmo3.ts",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- Server Logic ---
|
|
24
|
+
|
|
25
|
+
async function runServer() {
|
|
26
|
+
console.log(`š Starting Olmo-3 Server on port ${PORT}...`);
|
|
27
|
+
|
|
28
|
+
const cmd = [
|
|
29
|
+
BIN_PATH,
|
|
30
|
+
"-m",
|
|
31
|
+
MODEL_PATH,
|
|
32
|
+
"--port",
|
|
33
|
+
PORT.toString(),
|
|
34
|
+
"--ctx-size",
|
|
35
|
+
"8192", // Reduced context for stability on Metal
|
|
36
|
+
"--n-gpu-layers",
|
|
37
|
+
"99", // Offload to GPU/Metal
|
|
38
|
+
"--jinja", // Jinja2 template support
|
|
39
|
+
"--reasoning-format",
|
|
40
|
+
"deepseek", // Separate partial thinking
|
|
41
|
+
"-fa",
|
|
42
|
+
"on", // Flash Attention
|
|
43
|
+
"--temp",
|
|
44
|
+
"0.6",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const serverProcess = Bun.spawn(cmd, {
|
|
48
|
+
stdout: "inherit",
|
|
49
|
+
stderr: "inherit",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Wait for process to exit
|
|
53
|
+
await serverProcess.exited;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Dispatch ---
|
|
57
|
+
|
|
58
|
+
await lifecycle.run(command, runServer);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ServiceLifecycle } from "../utils/ServiceLifecycle";
|
|
3
|
+
|
|
4
|
+
// --- Configuration ---
|
|
5
|
+
const BASE_DIR = join(import.meta.dir, "../../experiments/enlightenment");
|
|
6
|
+
const BIN_PATH = join(BASE_DIR, "llama.cpp/build/bin/llama-server");
|
|
7
|
+
const MODEL_PATH = join(BASE_DIR, "vectors/Phi-3.5-mini-instruct-Q4_K_M.gguf");
|
|
8
|
+
|
|
9
|
+
const PORT = 8082;
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0] || "serve";
|
|
13
|
+
|
|
14
|
+
// --- Service Lifecycle ---
|
|
15
|
+
|
|
16
|
+
const lifecycle = new ServiceLifecycle({
|
|
17
|
+
name: "Phi-3.5",
|
|
18
|
+
pidFile: ".phi.pid",
|
|
19
|
+
logFile: ".phi.log",
|
|
20
|
+
entryPoint: "src/services/phi.ts",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- Server Logic ---
|
|
24
|
+
|
|
25
|
+
async function runServer() {
|
|
26
|
+
console.log(`š Starting Phi-3.5 Server on port ${PORT}...`);
|
|
27
|
+
|
|
28
|
+
const cmd = [
|
|
29
|
+
BIN_PATH,
|
|
30
|
+
"-m",
|
|
31
|
+
MODEL_PATH,
|
|
32
|
+
"--port",
|
|
33
|
+
PORT.toString(),
|
|
34
|
+
"--ctx-size",
|
|
35
|
+
"4096",
|
|
36
|
+
"--n-gpu-layers",
|
|
37
|
+
"99", // Offload to GPU/Metal
|
|
38
|
+
"--log-disable", // Keep clean for Phi
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const serverProcess = Bun.spawn(cmd, {
|
|
42
|
+
stdout: "inherit",
|
|
43
|
+
stderr: "inherit",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Wait for process to exit
|
|
47
|
+
await serverProcess.exited;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Dispatch ---
|
|
51
|
+
|
|
52
|
+
await lifecycle.run(command, runServer);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import settings from "../../polyvis.settings.json";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EnvironmentVerifier
|
|
7
|
+
*
|
|
8
|
+
* Responsible for verifying that the runtime environment matches the
|
|
9
|
+
* expectations set in polyvis.settings.json.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* await EnvironmentVerifier.verifyOrExit();
|
|
13
|
+
*/
|
|
14
|
+
export const EnvironmentVerifier = {
|
|
15
|
+
async verifyOrExit(): Promise<void> {
|
|
16
|
+
const errors: string[] = [];
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
console.error(`š”ļø [Env] Verifying filesystem context in: ${cwd}`);
|
|
20
|
+
|
|
21
|
+
// 1. Verify Database Directory
|
|
22
|
+
const dbPath = settings.paths.database.resonance;
|
|
23
|
+
const dbDir = resolve(cwd, dbPath, "..");
|
|
24
|
+
if (!existsSync(dbDir)) {
|
|
25
|
+
errors.push(`Database directory missing: ${dbDir}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Verify Source Directories
|
|
29
|
+
// We iterate over the 'sources' config to ensure target folders exist
|
|
30
|
+
const experienceSources = settings.paths.sources.experience;
|
|
31
|
+
for (const source of experienceSources) {
|
|
32
|
+
const absPath = resolve(cwd, source.path);
|
|
33
|
+
if (!existsSync(absPath)) {
|
|
34
|
+
console.warn(
|
|
35
|
+
` ā ļø Optional source directory missing: ${source.path} (created automatically by some tools, but worth noting)`,
|
|
36
|
+
);
|
|
37
|
+
// We don't hard fail on standard folders, as they might be empty/missing in a fresh repo.
|
|
38
|
+
// But specifically for 'docs' or critical ones we might want to be stricter.
|
|
39
|
+
// For now, we WARN.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Verify Static Assets (Lexicon/CDA)
|
|
44
|
+
const lexiconPath = resolve(cwd, settings.paths.sources.persona.lexicon);
|
|
45
|
+
if (!existsSync(lexiconPath)) {
|
|
46
|
+
errors.push(`Critical Artifact missing: Lexicon (${lexiconPath})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cdaPath = resolve(cwd, settings.paths.sources.persona.cda);
|
|
50
|
+
if (!existsSync(cdaPath)) {
|
|
51
|
+
errors.push(`Critical Artifact missing: CDA (${cdaPath})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (errors.length > 0) {
|
|
55
|
+
console.error("\nā Environment Verification Failed:");
|
|
56
|
+
errors.forEach((e) => {
|
|
57
|
+
console.error(` - ${e}`);
|
|
58
|
+
});
|
|
59
|
+
console.error(
|
|
60
|
+
"\nPlease ensure you are running from the project root and all assets are present.",
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.error(" ā
Environment Verified.");
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pino, { type Logger } from "pino";
|
|
2
|
+
export type { Logger };
|
|
3
|
+
|
|
4
|
+
// Configure the base logger
|
|
5
|
+
const loggerConfig: pino.LoggerOptions = {
|
|
6
|
+
level: process.env.LOG_LEVEL || "info",
|
|
7
|
+
base: undefined, // Remove pid/hostname from logs for cleanliness
|
|
8
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Default to stderr (fd 2) to prevent polluting stdout (critical for MCP/CLI piping)
|
|
12
|
+
export const rootLogger = pino(loggerConfig, pino.destination(2));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a child logger with a bound component name.
|
|
16
|
+
* @param component The name of the component (e.g., 'Ingestor', 'MCP')
|
|
17
|
+
* @returns A pino logger instance suitable for that component
|
|
18
|
+
*/
|
|
19
|
+
export const getLogger = (component: string) => {
|
|
20
|
+
return rootLogger.child({ component });
|
|
21
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { ZombieDefense } from "./ZombieDefense";
|
|
4
|
+
|
|
5
|
+
export interface ServiceConfig {
|
|
6
|
+
name: string; // e.g. "Daemon"
|
|
7
|
+
pidFile: string; // e.g. ".daemon.pid"
|
|
8
|
+
logFile: string; // e.g. ".daemon.log"
|
|
9
|
+
entryPoint: string; // e.g. "src/resonance/daemon.ts"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ServiceLifecycle {
|
|
13
|
+
constructor(private config: ServiceConfig) {}
|
|
14
|
+
|
|
15
|
+
private async isRunning(pid: number): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
} catch (_e) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the service in the background (detached).
|
|
26
|
+
*/
|
|
27
|
+
async start() {
|
|
28
|
+
// Enforce clean state first (kill duplicates)
|
|
29
|
+
await ZombieDefense.assertClean(this.config.name, true);
|
|
30
|
+
|
|
31
|
+
// Check if already running based on PID file
|
|
32
|
+
if (await Bun.file(this.config.pidFile).exists()) {
|
|
33
|
+
const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
|
|
34
|
+
if (await this.isRunning(pid)) {
|
|
35
|
+
console.log(`ā ļø ${this.config.name} is already running (PID: ${pid})`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(
|
|
39
|
+
`ā ļø Found stale PID file for ${this.config.name}. Clearing...`,
|
|
40
|
+
);
|
|
41
|
+
await unlink(this.config.pidFile);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const logFile = Bun.file(this.config.logFile);
|
|
45
|
+
await Bun.write(logFile, ""); // Truncate logs
|
|
46
|
+
|
|
47
|
+
// Spawn subprocess
|
|
48
|
+
const subprocess = Bun.spawn(
|
|
49
|
+
["bun", "run", this.config.entryPoint, "serve"],
|
|
50
|
+
{
|
|
51
|
+
cwd: process.cwd(),
|
|
52
|
+
detached: true,
|
|
53
|
+
stdout: logFile,
|
|
54
|
+
stderr: logFile,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await Bun.write(this.config.pidFile, subprocess.pid.toString());
|
|
59
|
+
subprocess.unref();
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`ā
${this.config.name} started in background (PID: ${subprocess.pid})`,
|
|
63
|
+
);
|
|
64
|
+
console.log(`š Logs: ${this.config.logFile}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stop the service using the PID file.
|
|
69
|
+
*/
|
|
70
|
+
async stop() {
|
|
71
|
+
if (!(await Bun.file(this.config.pidFile).exists())) {
|
|
72
|
+
console.log(`ā¹ļø ${this.config.name} is not running.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
|
|
77
|
+
|
|
78
|
+
if (await this.isRunning(pid)) {
|
|
79
|
+
console.log(`š Stopping ${this.config.name} (PID: ${pid})...`);
|
|
80
|
+
process.kill(pid, "SIGTERM");
|
|
81
|
+
|
|
82
|
+
let attempts = 0;
|
|
83
|
+
// Wait up to 1 second
|
|
84
|
+
while ((await this.isRunning(pid)) && attempts < 10) {
|
|
85
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
86
|
+
attempts++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (await this.isRunning(pid)) {
|
|
90
|
+
console.log("ā ļø Process did not exit gracefully. Force killing...");
|
|
91
|
+
process.kill(pid, "SIGKILL");
|
|
92
|
+
}
|
|
93
|
+
console.log(`ā
${this.config.name} stopped.`);
|
|
94
|
+
} else {
|
|
95
|
+
console.log("ā ļø Stale PID file found. Cleaning up.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await unlink(this.config.pidFile);
|
|
100
|
+
} catch (e: unknown) {
|
|
101
|
+
const err = e as { code?: string; message: string };
|
|
102
|
+
if (err.code !== "ENOENT") {
|
|
103
|
+
console.warn(`ā ļø Failed to remove PID file: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check status of the service.
|
|
110
|
+
*/
|
|
111
|
+
async status() {
|
|
112
|
+
if (await Bun.file(this.config.pidFile).exists()) {
|
|
113
|
+
const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
|
|
114
|
+
if (await this.isRunning(pid)) {
|
|
115
|
+
console.log(`š¢ ${this.config.name} is RUNNING (PID: ${pid})`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(`š“ ${this.config.name} is NOT RUNNING (Stale PID: ${pid})`);
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`āŖļø ${this.config.name} is STOPPED`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wrapper for the foreground 'serve' command logic.
|
|
126
|
+
* Use this to wrap your actual server startup code.
|
|
127
|
+
*/
|
|
128
|
+
async serve(serverLogic: () => Promise<void>, checkZombies = true) {
|
|
129
|
+
// Enforce clean state (ensure we aren't running as a zombie of ourselves)
|
|
130
|
+
if (checkZombies) {
|
|
131
|
+
await ZombieDefense.assertClean(`${this.config.name} (Serve)`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Register cleanup handlers to remove PID file on exit/crash/kill
|
|
135
|
+
let cleanupCalled = false;
|
|
136
|
+
const cleanup = async (signal?: string) => {
|
|
137
|
+
if (cleanupCalled) return; // Prevent double cleanup
|
|
138
|
+
cleanupCalled = true;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (await Bun.file(this.config.pidFile).exists()) {
|
|
142
|
+
await unlink(this.config.pidFile);
|
|
143
|
+
if (signal) {
|
|
144
|
+
console.error(
|
|
145
|
+
`\nš§¹ ${this.config.name}: PID file cleaned up on ${signal}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (_e) {
|
|
150
|
+
// Ignore cleanup errors (file might already be deleted)
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Register signal handlers
|
|
155
|
+
process.on("SIGINT", () => cleanup("SIGINT").then(() => process.exit(0)));
|
|
156
|
+
process.on("SIGTERM", () => cleanup("SIGTERM").then(() => process.exit(0)));
|
|
157
|
+
process.on("exit", () => {
|
|
158
|
+
// Note: exit event is synchronous, so we do sync cleanup
|
|
159
|
+
if (!cleanupCalled && existsSync(this.config.pidFile)) {
|
|
160
|
+
cleanupCalled = true;
|
|
161
|
+
try {
|
|
162
|
+
Bun.write(this.config.pidFile, ""); // Truncate to mark as stale
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await serverLogic();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Main CLI dispatch logic.
|
|
172
|
+
*/
|
|
173
|
+
async run(
|
|
174
|
+
command: string,
|
|
175
|
+
serverLogic: () => Promise<void>,
|
|
176
|
+
checkZombies = true,
|
|
177
|
+
) {
|
|
178
|
+
switch (command) {
|
|
179
|
+
case "start":
|
|
180
|
+
await this.start();
|
|
181
|
+
process.exit(0);
|
|
182
|
+
break;
|
|
183
|
+
case "stop":
|
|
184
|
+
await this.stop();
|
|
185
|
+
process.exit(0);
|
|
186
|
+
break;
|
|
187
|
+
case "status":
|
|
188
|
+
await this.status();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
break;
|
|
191
|
+
case "restart":
|
|
192
|
+
await this.stop();
|
|
193
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
194
|
+
await this.start();
|
|
195
|
+
process.exit(0);
|
|
196
|
+
break;
|
|
197
|
+
case "serve":
|
|
198
|
+
await this.serve(serverLogic, checkZombies);
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
console.log(
|
|
202
|
+
`Unknown command '${command}'. Use: start, stop, status, restart, or serve`,
|
|
203
|
+
);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
export interface ZombieReport {
|
|
7
|
+
ghosts: string[];
|
|
8
|
+
duplicates: string[];
|
|
9
|
+
unknowns: string[];
|
|
10
|
+
clean: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Services intended to run as singletons
|
|
14
|
+
const WHITELIST = [
|
|
15
|
+
"src/resonance/daemon.ts",
|
|
16
|
+
"src/mcp/index.ts",
|
|
17
|
+
"scripts/cli/dev.ts",
|
|
18
|
+
"src/resonance/cli/ingest.ts",
|
|
19
|
+
"bun run build:data",
|
|
20
|
+
"bun run mcp",
|
|
21
|
+
"bun run daemon",
|
|
22
|
+
"bun run dev",
|
|
23
|
+
"bun run watch:css",
|
|
24
|
+
"bun run watch:js",
|
|
25
|
+
"scripts/verify/test_mcp_query.ts",
|
|
26
|
+
"src/services/olmo3.ts",
|
|
27
|
+
"src/services/phi.ts",
|
|
28
|
+
"src/services/llama.ts",
|
|
29
|
+
"src/services/llamauv.ts",
|
|
30
|
+
"scripts/cli/servers.ts",
|
|
31
|
+
"bun run olmo3",
|
|
32
|
+
"bun run phi",
|
|
33
|
+
"bun run llama",
|
|
34
|
+
"bun run llamauv",
|
|
35
|
+
"bun run servers",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const ZombieDefense = {
|
|
39
|
+
/**
|
|
40
|
+
* Scan the environment for unauthorized or stale processes.
|
|
41
|
+
*/
|
|
42
|
+
async scan(excludePids: string[] = []): Promise<ZombieReport> {
|
|
43
|
+
const report: ZombieReport = {
|
|
44
|
+
ghosts: [],
|
|
45
|
+
duplicates: [],
|
|
46
|
+
unknowns: [],
|
|
47
|
+
clean: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const protectedPids = new Set([
|
|
51
|
+
process.pid.toString(),
|
|
52
|
+
process.ppid.toString(),
|
|
53
|
+
...excludePids,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// 1. Ghost Check (Deleted File Handles)
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execAsync("lsof +L1");
|
|
59
|
+
const lines = stdout.split("\n");
|
|
60
|
+
// Strict Filter: Only worry about resonance.db or files in our project scope
|
|
61
|
+
report.ghosts = lines.filter((l) => {
|
|
62
|
+
if (!l.includes("bun")) return false;
|
|
63
|
+
// Must be relevant file
|
|
64
|
+
return l.includes("resonance.db") || l.includes(process.cwd());
|
|
65
|
+
});
|
|
66
|
+
} catch (_e) {
|
|
67
|
+
// lsof exits 1 if nothing found
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Process Table Check
|
|
71
|
+
try {
|
|
72
|
+
const { stdout } = await execAsync("ps aux | grep bun | grep -v grep");
|
|
73
|
+
const processes = stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
74
|
+
const activeMap = new Map<string, number>();
|
|
75
|
+
|
|
76
|
+
processes.forEach((p) => {
|
|
77
|
+
// Ignore self and parent immediately
|
|
78
|
+
const match = p.match(/\s+(\d+)\s+/);
|
|
79
|
+
if (match?.[1] && protectedPids.has(match[1])) return;
|
|
80
|
+
|
|
81
|
+
// Strict Filter: Must be in our CWD or explicit bun run
|
|
82
|
+
if (!p.includes(process.cwd()) && !p.includes("bun run")) return;
|
|
83
|
+
|
|
84
|
+
const isWhitelisted = WHITELIST.some((w) => p.includes(w));
|
|
85
|
+
|
|
86
|
+
if (isWhitelisted) {
|
|
87
|
+
WHITELIST.forEach((w) => {
|
|
88
|
+
if (p.includes(w)) {
|
|
89
|
+
// HEURISTIC: Don't count "bun run scripts/foo.ts" and "bun scripts/foo.ts" as duplicates of each other if they are the same PID (obviously),
|
|
90
|
+
// but here we already filtered by PID.
|
|
91
|
+
// We need to be careful about the wrapper vs the actual process.
|
|
92
|
+
|
|
93
|
+
const count = (activeMap.get(w) || 0) + 1;
|
|
94
|
+
activeMap.set(w, count);
|
|
95
|
+
if (count > 1) {
|
|
96
|
+
report.duplicates.push(p);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
// Ignore self (if running from a script calling this)
|
|
102
|
+
if (
|
|
103
|
+
!p.includes("detect_zombies.ts") &&
|
|
104
|
+
!p.includes("ZombieDefense")
|
|
105
|
+
) {
|
|
106
|
+
report.unknowns.push(p);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
} catch (_e) {
|
|
111
|
+
// No bun processes
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
report.ghosts.length > 0 ||
|
|
116
|
+
report.duplicates.length > 0 ||
|
|
117
|
+
report.unknowns.length > 0
|
|
118
|
+
) {
|
|
119
|
+
report.clean = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return report;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Helper: Extract PIDs from report lines
|
|
127
|
+
*/
|
|
128
|
+
extractPids(lines: string[]): string[] {
|
|
129
|
+
const pids = new Set<string>();
|
|
130
|
+
lines.forEach((line) => {
|
|
131
|
+
const match = line.match(/\s+(\d+)\s+/);
|
|
132
|
+
if (match?.[1]) {
|
|
133
|
+
pids.add(match[1]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return Array.from(pids);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Terminate identified zombie PIDs
|
|
141
|
+
*/
|
|
142
|
+
async killZombies(report: ZombieReport) {
|
|
143
|
+
let targets = [
|
|
144
|
+
...new Set([
|
|
145
|
+
...this.extractPids(report.ghosts),
|
|
146
|
+
...this.extractPids(report.duplicates),
|
|
147
|
+
]),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// SAFETY: Exclude self
|
|
151
|
+
const selfPid = process.pid.toString();
|
|
152
|
+
targets = targets.filter((pid) => pid !== selfPid);
|
|
153
|
+
|
|
154
|
+
if (targets.length === 0) return;
|
|
155
|
+
|
|
156
|
+
console.error(
|
|
157
|
+
`šŖ Killing ${targets.length} zombie processes: ${targets.join(", ")}`,
|
|
158
|
+
);
|
|
159
|
+
try {
|
|
160
|
+
await execAsync(`kill -9 ${targets.join(" ")}`);
|
|
161
|
+
console.error(" ā
Zombies terminated.");
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const e = err as Error;
|
|
164
|
+
console.error(` ā Failed to kill zombies: ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Enforce a clean state. Exits process if zombies found.
|
|
170
|
+
* @param serviceName Name of the service calling this guard
|
|
171
|
+
* @param interactive If true, prompts user to kill zombies.
|
|
172
|
+
*/
|
|
173
|
+
async assertClean(serviceName: string, interactive = false) {
|
|
174
|
+
if (process.env.SKIP_ZOMBIE_CHECK === "true") {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.error(`š”ļø [${serviceName}] Running Zombie Defense Protocol...`);
|
|
178
|
+
let report = await this.scan();
|
|
179
|
+
|
|
180
|
+
if (report.clean) {
|
|
181
|
+
console.error(" ā
Environment Clean.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.error("\nš STARTUP ABORTED: ZOMBIE PROCESSES DETECTED");
|
|
186
|
+
|
|
187
|
+
if (report.ghosts.length > 0) {
|
|
188
|
+
console.error("\nš» GHOSTS (Holding deleted files):");
|
|
189
|
+
report.ghosts.forEach((g) => {
|
|
190
|
+
console.error(` ${g}`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (report.duplicates.length > 0) {
|
|
195
|
+
console.error("\nšÆ DUPLICATES (Service already running?):");
|
|
196
|
+
report.duplicates.forEach((d) => {
|
|
197
|
+
console.error(` ${d}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (report.unknowns.length > 0) {
|
|
202
|
+
console.error(
|
|
203
|
+
"\nš½ UNKNOWNS (Rogue processes - Manual Check Recommended):",
|
|
204
|
+
);
|
|
205
|
+
report.unknowns.forEach((u) => {
|
|
206
|
+
console.error(` ${u}`);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (interactive) {
|
|
211
|
+
const targets = this.extractPids([
|
|
212
|
+
...report.ghosts,
|
|
213
|
+
...report.duplicates,
|
|
214
|
+
]);
|
|
215
|
+
if (targets.length > 0) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`\nš Found ${targets.length} confirmable zombies. Kill and Proceed? [y/N] `,
|
|
218
|
+
);
|
|
219
|
+
const answer = await new Promise<string>((resolve) => {
|
|
220
|
+
// Simple one-off prompt since Bun used here might not have 'prompt' in all envs
|
|
221
|
+
// Using Bun.stdin reader
|
|
222
|
+
process.stdin.once("data", (d) => resolve(d.toString().trim()));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
226
|
+
await this.killZombies(report);
|
|
227
|
+
// Re-scan to verify
|
|
228
|
+
report = await this.scan();
|
|
229
|
+
if (report.clean) {
|
|
230
|
+
console.error(" ā
Environment Cleared. Proceeding...");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.error(" ā Environment still dirty after kill attempt.");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
console.error(
|
|
238
|
+
"\nš ACTION REQUIRED: Run 'pkill -f bun' to clear the environment.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
process.exit(1);
|
|
243
|
+
},
|
|
244
|
+
};
|