Iostream-Li's picture
Add files using upload-large-folder tool
5871090 verified
import app from "./app";
import { logger } from "./lib/logger";
import { ensureLoaded } from "./lib/research-data";
import { startJobPoller } from "./lib/job-tracker";
import { startLatencyAlertMonitor } from "./llm/latency-alert-monitor";
import {
loadPersistedLatencySamples,
startLatencySamplePersistence,
} from "./llm/sample-persistence";
import { config } from "./lib/config";
import { db } from "@workspace/db";
import { inviteCodes } from "@workspace/db";
import { sql } from "drizzle-orm";
import { seedToolGraphOnce } from "./lib/tool-graph-seed";
import { seedDrugClipOnce } from "./lib/drugclip/seed";
import {
toolGraphEnabled,
startDeprecationScheduler,
} from "./lib/tool-graph";
import { startEvolutionScheduler } from "./lib/evolution/scheduler";
import { applyCapabilityMigrations } from "./lib/capability/migrations/apply.js";
import { loadRegisteredCapabilities } from "./lib/capability/loader.js";
// Production sanity check: if neither the env-derived invite list nor any
// row in `invite_codes` is present, brand-new sign-ups will all 403. Log a
// loud warning so operators notice before users do. We do not fail boot —
// the seed step (`pnpm --filter @workspace/api-server seed:prod`) is the
// supported recovery path and may be run after the server is up.
if (process.env["NODE_ENV"] === "production" && config.inviteCodes.length === 0) {
void (async () => {
try {
const rows = await db
.select({ n: sql<number>`count(*)::int` })
.from(inviteCodes);
const count = rows[0]?.n ?? 0;
if (count === 0) {
logger.error(
"AUTH BOOTSTRAP: no invite codes available (INVITE_CODES env is empty AND invite_codes table is empty). " +
"All registrations will return 403. Run `pnpm --filter @workspace/api-server seed:prod` " +
"with INITIAL_INVITE_CODES set, or populate the INVITE_CODES env var, then redeploy.",
);
}
} catch (err) {
logger.warn(
{ err },
"AUTH BOOTSTRAP: could not query invite_codes table to verify auth bootstrap state.",
);
}
})();
}
// Warm research data cache in background after server starts.
void ensureLoaded().catch((err) =>
logger.error({ err }, "research data warm-load failed"),
);
// Idempotently seed the cross-process tool capability graph (Task #147).
// Safe to call on every boot — the seed importer no-ops if the curated
// nodes are already present and verified.
if (toolGraphEnabled() && process.env["NODE_ENV"] !== "test") {
void seedToolGraphOnce().catch((err) =>
logger.warn({ err }, "tool-graph seed failed (continuing without graph)"),
);
}
// Seed first-class tool networks (Task #176, Wave A). DrugCLIP is the
// inaugural network; future networks register here too. Idempotent.
if (process.env["NODE_ENV"] !== "test") {
void seedDrugClipOnce().catch((err) =>
logger.warn({ err }, "drugclip seed failed (continuing without network)"),
);
}
// Begin polling the research engine for in-flight async jobs so we can
// broadcast completion events and post the proactive follow-up message.
startJobPoller();
// Watch provider-gateway latency and broadcast an announcement when a
// provider's p95 has been near its timeout ceiling for several minutes.
// Skip during tests so suites don't have to teardown a global timer.
if (process.env["NODE_ENV"] !== "test") {
startLatencyAlertMonitor();
// Restore the persisted recent-samples window so the admin sparkline
// (Task #127) is populated immediately after a restart instead of
// waiting on fresh traffic, then start the debounced flush loop that
// mirrors new in-memory samples back to Postgres.
void loadPersistedLatencySamples()
.catch((err) =>
logger.warn({ err }, "loadPersistedLatencySamples failed"),
)
.finally(() => {
startLatencySamplePersistence();
});
}
const rawPort = process.env["PORT"];
if (!rawPort) {
throw new Error(
"PORT environment variable is required but was not provided.",
);
}
const port = Number(rawPort);
if (Number.isNaN(port) || port <= 0) {
throw new Error(`Invalid PORT value: "${rawPort}"`);
}
const server = app.listen(port, () => {
logger.info({ port }, "Server listening");
// Task #255 / B2 — 启动时打印 7 类外部知识适配器状态(real / interface_only)。
void import("./lib/capability/external-knowledge/index.ts").then((m) =>
m.logAdapterStatusBanner({ info: (msg) => logger.info(msg) }),
);
// Task #242 / B1 — 把 capability schema 拉到当前版本(idempotent),
// 然后扫 _registered/*.manifest.{ts,js,mjs} 自动注册 capability。
// 顺序很重要:loader 里的 manifest 可能直接 insert capability 行,
// 必须先确保 capabilities / capability_lifecycle_events 表存在。
void applyCapabilityMigrations()
.then(({ applied, skipped }) =>
logger.info(
{ applied, skipped },
"capability schema migrations applied",
),
)
.then(() => loadRegisteredCapabilities())
.then(({ loaded, errors }) => {
if (loaded.length > 0 || errors.length > 0) {
logger.info(
{ loaded, errors },
"capability registry boot scan complete",
);
} else {
logger.info(
"capability registry boot scan complete (0 manifests in _registered/)",
);
}
})
.catch((err) =>
logger.error(
{ err },
"capability boot pipeline failed (server still up; capability layer degraded)",
),
);
});
// Tool-graph deprecation scheduler (#159). No-op unless
// DEPRECATION_DETECTOR_ENABLED=1; safe to leave enabled in production.
if (toolGraphEnabled()) {
startDeprecationScheduler();
}
// Online-evolution flywheel cadence worker (Task #181). Calls
// evaluateTriggers + attemptAutoPromote + tickRollbackWatch on a fixed
// schedule so the loop runs without an operator hitting the admin
// page. Honours EVOLUTION_SCHEDULER_DISABLED=1 and is inert in tests.
if (process.env["NODE_ENV"] !== "test") {
startEvolutionScheduler();
}
server.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
logger.fatal(
{ err, port },
`Port ${port} is already in use. Another process is bound to it; refusing to start.`,
);
} else if (err.code === "EACCES") {
logger.fatal(
{ err, port },
`Permission denied binding to port ${port}.`,
);
} else {
logger.fatal({ err, port }, "Fatal error while starting HTTP server");
}
process.exit(1);
});