import { isRecord } from "../../model/config/schema.js";
import type { ModelConfig } from "../../model/protocol/canonical.js";
import {
PilotConfigError,
type PilotConfigDiagnostic,
type PilotMemoryApiType,
type PilotMemoryConfig,
type PilotMemoryReasoningMode,
type PilotMemoryScheduleConfig,
} from "./types.js";
export function parseMemoryConfig(
rawMemory: unknown,
diagnostics: PilotConfigDiagnostic[],
defaultRootDir: string,
modelConfig?: ModelConfig,
): PilotMemoryConfig | undefined {
if (rawMemory === undefined) {
return undefined;
}
if (!isRecord(rawMemory)) {
diagnostics.push({
code: "CONFIG_MEMORY_INVALID",
severity: "fatal",
message: "memory config must be an object.",
path: "memory",
recoverable: false,
});
return undefined;
}
const enabled = readBoolean(rawMemory.enabled, true, "memory.enabled");
const provider = readString(rawMemory.provider, "edgeclaw", "memory.provider");
if (provider !== "edgeclaw") {
diagnostics.push({
code: "CONFIG_MEMORY_PROVIDER_UNSUPPORTED",
severity: "fatal",
message: `Unsupported memory provider ${provider}.`,
path: "memory.provider",
recoverable: false,
});
return undefined;
}
const memoryModel = parseMemoryModelRef(rawMemory.model, diagnostics, modelConfig);
const schedule = parseMemorySchedule(rawMemory.schedule, diagnostics);
for (const key of Object.keys(rawMemory)) {
if (
key !== "enabled"
&& key !== "provider"
&& key !== "rootDir"
&& key !== "captureStrategy"
&& key !== "includeAssistant"
&& key !== "maxMessageChars"
&& key !== "retrievalTimeoutMs"
&& key !== "model"
&& key !== "apiType"
&& key !== "schedule"
&& key !== "heartbeatBatchSize"
) {
diagnostics.push({
code: "CONFIG_MEMORY_UNKNOWN_FIELD",
severity: "warning",
message: `Unknown memory field ${key}.`,
path: `memory.${key}`,
recoverable: true,
});
}
}
return {
enabled,
provider,
rootDir: readOptionalString(rawMemory.rootDir, "memory.rootDir") ?? defaultRootDir,
captureStrategy: readCaptureStrategy(rawMemory.captureStrategy),
includeAssistant: readBoolean(rawMemory.includeAssistant, true, "memory.includeAssistant"),
maxMessageChars: readOptionalPositiveNumber(rawMemory.maxMessageChars, "memory.maxMessageChars"),
retrievalTimeoutMs: readOptionalPositiveInteger(
rawMemory.retrievalTimeoutMs,
"memory.retrievalTimeoutMs",
),
model: memoryModel,
apiType: readMemoryApiType(rawMemory.apiType),
schedule,
heartbeatBatchSize: readOptionalPositiveInteger(rawMemory.heartbeatBatchSize, "memory.heartbeatBatchSize"),
};
}
function parseMemorySchedule(
value: unknown,
diagnostics: PilotConfigDiagnostic[],
): PilotMemoryScheduleConfig | undefined {
if (value === undefined) {
return undefined;
}
if (!isRecord(value)) {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", "memory.schedule must be an object.");
}
const schedule: PilotMemoryScheduleConfig = {};
const reasoningMode = readOptionalMemoryReasoningMode(value.reasoningMode);
if (reasoningMode !== undefined) schedule.reasoningMode = reasoningMode;
const autoIndexIntervalMinutes = readOptionalNonNegativeInteger(
value.autoIndexIntervalMinutes,
"memory.schedule.autoIndexIntervalMinutes",
);
if (autoIndexIntervalMinutes !== undefined) {
schedule.autoIndexIntervalMinutes = autoIndexIntervalMinutes;
}
const autoDreamIntervalMinutes = readOptionalNonNegativeInteger(
value.autoDreamIntervalMinutes,
"memory.schedule.autoDreamIntervalMinutes",
);
if (autoDreamIntervalMinutes !== undefined) {
schedule.autoDreamIntervalMinutes = autoDreamIntervalMinutes;
}
for (const key of Object.keys(value)) {
if (key !== "reasoningMode" && key !== "autoIndexIntervalMinutes" && key !== "autoDreamIntervalMinutes") {
diagnostics.push({
code: "CONFIG_MEMORY_UNKNOWN_FIELD",
severity: "warning",
message: `Unknown memory.schedule field ${key}.`,
path: `memory.schedule.${key}`,
recoverable: true,
});
}
}
return Object.keys(schedule).length > 0 ? schedule : undefined;
}
function parseMemoryModelRef(
value: unknown,
diagnostics: PilotConfigDiagnostic[],
modelConfig?: ModelConfig,
): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string" || value.length === 0) {
throw new PilotConfigError(
"CONFIG_MEMORY_MODEL_INVALID",
'memory.model must be a "provider/model" string.',
);
}
const sep = value.indexOf("/");
if (sep < 0) {
throw new PilotConfigError(
"CONFIG_MEMORY_MODEL_INVALID",
'memory.model must use "provider/model" format.',
);
}
if (modelConfig) {
const providerId = value.slice(0, sep);
const modelId = value.slice(sep + 1);
if (!modelConfig.providers[providerId]) {
diagnostics.push({
code: "CONFIG_MEMORY_MODEL_PROVIDER_NOT_FOUND",
severity: "warning",
message: `memory.model references unknown provider ${providerId}.`,
path: "memory.model",
recoverable: true,
});
} else if (!modelConfig.providers[providerId].models[modelId]) {
diagnostics.push({
code: "CONFIG_MEMORY_MODEL_NOT_FOUND",
severity: "warning",
message: `memory.model references unknown model ${modelId} for provider ${providerId}.`,
path: "memory.model",
recoverable: true,
});
}
}
return value;
}
function readMemoryApiType(value: unknown): PilotMemoryApiType | undefined {
if (value === undefined) {
return undefined;
}
if (value === "openai-responses" || value === "responses" || value === "openai-completions") {
return value;
}
throw new PilotConfigError(
"CONFIG_MEMORY_VALUE_INVALID",
"memory.apiType must be openai-responses, responses, or openai-completions.",
);
}
function readOptionalMemoryReasoningMode(value: unknown): PilotMemoryReasoningMode | undefined {
if (value === undefined) {
return undefined;
}
if (value === "answer_first" || value === "accuracy_first") {
return value;
}
throw new PilotConfigError(
"CONFIG_MEMORY_VALUE_INVALID",
"memory.schedule.reasoningMode must be answer_first or accuracy_first.",
);
}
function readCaptureStrategy(value: unknown): PilotMemoryConfig["captureStrategy"] {
if (value === undefined) {
return "last_turn";
}
if (value === "last_turn" || value === "full_session") {
return value;
}
throw new PilotConfigError("CONFIG_MEMORY_CAPTURE_INVALID", "memory.captureStrategy must be last_turn or full_session.");
}
function readString(value: unknown, fallback: string, path: string): string {
if (value === undefined) {
return fallback;
}
if (typeof value !== "string" || value.length === 0) {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", `${path} must be a non-empty string.`);
}
return value;
}
function readOptionalString(value: unknown, path: string): string | undefined {
if (value === undefined) {
return undefined;
}
return readString(value, "", path);
}
function readBoolean(value: unknown, fallback: boolean, path: string): boolean {
if (value === undefined) {
return fallback;
}
if (typeof value !== "boolean") {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", `${path} must be a boolean.`);
}
return value;
}
function readOptionalPositiveNumber(value: unknown, path: string): number | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", `${path} must be a positive number.`);
}
return value;
}
function readOptionalPositiveInteger(value: unknown, path: string): number | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", `${path} must be a positive number.`);
}
return Math.floor(value);
}
function readOptionalNonNegativeInteger(value: unknown, path: string): number | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
throw new PilotConfigError("CONFIG_MEMORY_VALUE_INVALID", `${path} must be a non-negative number.`);
}
return Math.floor(value);
}