import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; export interface CtxVarsConfig { enabled: boolean; /** Populate the variable store on the fly as messages happen (content only; * decisions/summaries/dependencies still happen at compaction time). */ realtimePopulation: boolean; /** Archive section soft threshold: when more than this many archive * summaries are individually represented, the compaction agent is asked * to roll the oldest ones up. */ archiveKeepIndividual: number; /** Per-entry summary cap (chars). */ summaryMaxChars: number; /** Target token budget for the model-visible context snapshot. Verbatim * pinned content is accounted separately and is never silently reduced. */ summaryTokenBudget: number; /** Maximum number of synthesized active-topic sections. */ snapshotMaxTopics: number; /** Historical rollup summary cap (chars). */ rollupMaxChars: number; /** Total compaction packet budget in tokens (input to compaction agent). */ packetTokenBudget: number; /** Truncation for tool-result text in the candidate packet. */ toolResultTruncateChars: number; /** Truncation for other candidate text in the packet. */ candidateTruncateChars: number; /** Legacy-named warning threshold for large verbatim pins. Pins are not * truncated; persistent content remains exact. */ pinnedMaxChars: number; /** Model for the compaction agent: "provider/model" or null to use the session model. */ compactionModel: string | null; /** Max output tokens for the compaction agent call. */ compactionMaxTokens: number; /** Query result row cap. */ queryRowCap: number; /** Override store directory (tests). */ storeDir: string | null; debug: boolean; debugLogPath: string | null; } const DEFAULTS: CtxVarsConfig = { enabled: true, realtimePopulation: true, archiveKeepIndividual: 30, summaryMaxChars: 500, summaryTokenBudget: 3500, snapshotMaxTopics: 10, rollupMaxChars: 2000, packetTokenBudget: 15000, toolResultTruncateChars: 4000, candidateTruncateChars: 3000, pinnedMaxChars: 40000, compactionModel: null, compactionMaxTokens: 4000, queryRowCap: 50, storeDir: null, debug: false, debugLogPath: null, }; function readJson(path: string): Record | null { try { if (!existsSync(path)) return null; return JSON.parse(readFileSync(path, "utf8")) as Record; } catch { return null; } } function pick(obj: Record | null, key: string, fallback: T): T { if (!obj) return fallback; const v = obj[key]; return v === undefined || v === null ? fallback : (v as T); } export function loadConfig(): CtxVarsConfig { const home = homedir(); const globalSettings = readJson(join(home, ".pi", "agent", "settings.json")); const projectSettings = readJson(join(process.cwd(), ".pi", "settings.json")); // Deep-merge contextVars sections: project wins over global. const base = { ...DEFAULTS }; for (const settings of [globalSettings, projectSettings]) { const section = settings ? pick | null>(settings, "contextVars", null) : null; if (!section) continue; for (const [k, v] of Object.entries(section)) { if (v !== undefined && v !== null) (base as Record)[k] = v; } } // Env overrides. const env = process.env; const cfg: CtxVarsConfig = { ...base, enabled: env.CTXV_ENABLED ? env.CTXV_ENABLED === "1" : base.enabled, realtimePopulation: env.CTXV_REALTIME ? env.CTXV_REALTIME === "1" : base.realtimePopulation, archiveKeepIndividual: env.CTXV_KEEP_INDIVIDUAL ? Number(env.CTXV_KEEP_INDIVIDUAL) : base.archiveKeepIndividual, summaryMaxChars: env.CTXV_SUMMARY_MAX ? Number(env.CTXV_SUMMARY_MAX) : base.summaryMaxChars, summaryTokenBudget: env.CTXV_SUMMARY_TOKENS ? Number(env.CTXV_SUMMARY_TOKENS) : base.summaryTokenBudget, snapshotMaxTopics: env.CTXV_SNAPSHOT_TOPICS ? Number(env.CTXV_SNAPSHOT_TOPICS) : base.snapshotMaxTopics, rollupMaxChars: env.CTXV_ROLLUP_MAX ? Number(env.CTXV_ROLLUP_MAX) : base.rollupMaxChars, packetTokenBudget: env.CTXV_PACKET_TOKENS ? Number(env.CTXV_PACKET_TOKENS) : base.packetTokenBudget, toolResultTruncateChars: env.CTXV_TOOL_TRUNCATE ? Number(env.CTXV_TOOL_TRUNCATE) : base.toolResultTruncateChars, candidateTruncateChars: env.CTXV_CAND_TRUNCATE ? Number(env.CTXV_CAND_TRUNCATE) : base.candidateTruncateChars, pinnedMaxChars: env.CTXV_PIN_MAX ? Number(env.CTXV_PIN_MAX) : base.pinnedMaxChars, compactionModel: env.CTXV_COMPACTION_MODEL ? env.CTXV_COMPACTION_MODEL : base.compactionModel, compactionMaxTokens: env.CTXV_COMPACTION_MAX_TOKENS ? Number(env.CTXV_COMPACTION_MAX_TOKENS) : base.compactionMaxTokens, storeDir: env.CTXV_STORE_DIR ? env.CTXV_STORE_DIR : base.storeDir, debug: env.CTXV_DEBUG ? env.CTXV_DEBUG === "1" : base.debug, debugLogPath: env.CTXV_DEBUG_LOG ? env.CTXV_DEBUG_LOG : base.debugLogPath, }; return cfg; } let _debugLog: string | null = null; export function setDebugLog(path: string | null) { _debugLog = path; } export function dbg(msg: string) { if (!_debugLog) return; try { const { appendFileSync } = require("node:fs") as typeof import("node:fs"); appendFileSync(_debugLog, `${new Date().toISOString()} ${msg}\n`); } catch { // ignore } }