mirror of https://github.com/openclaw/openclaw.git
memory-core: checkpoint mode-first dreaming refactor
This commit is contained in:
parent
5a42355d54
commit
02f2a66dff
|
|
@ -1,7 +1,6 @@
|
|||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { registerMemoryCli } from "./src/cli.js";
|
||||
import { registerDreamingCommand } from "./src/dreaming-command.js";
|
||||
import { registerMemoryDreamingPhases } from "./src/dreaming-phases.js";
|
||||
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
|
||||
import {
|
||||
buildMemoryFlushPlan,
|
||||
|
|
@ -29,7 +28,6 @@ export default definePluginEntry({
|
|||
register(api) {
|
||||
registerBuiltInMemoryEmbeddingProviders(api);
|
||||
registerShortTermPromotionDreaming(api);
|
||||
registerMemoryDreamingPhases(api);
|
||||
registerDreamingCommand(api);
|
||||
api.registerMemoryPromptSection(buildPromptSection);
|
||||
api.registerMemoryFlushPlan(buildMemoryFlushPlan);
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe("short-term dreaming config", () => {
|
|||
cfg,
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
cron: constants.DEFAULT_DREAMING_CRON_EXPR,
|
||||
timezone: "America/Los_Angeles",
|
||||
limit: constants.DEFAULT_DREAMING_LIMIT,
|
||||
|
|
@ -141,6 +141,7 @@ describe("short-term dreaming config", () => {
|
|||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
timezone: "UTC",
|
||||
verboseLogging: true,
|
||||
phases: {
|
||||
|
|
@ -179,6 +180,7 @@ describe("short-term dreaming config", () => {
|
|||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
phases: {
|
||||
deep: {
|
||||
cron: "5 1 * * *",
|
||||
|
|
@ -214,6 +216,7 @@ describe("short-term dreaming config", () => {
|
|||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
phases: {
|
||||
deep: {
|
||||
limit: " ",
|
||||
|
|
@ -248,6 +251,7 @@ describe("short-term dreaming config", () => {
|
|||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
phases: {
|
||||
deep: {
|
||||
limit: 0,
|
||||
|
|
@ -263,6 +267,7 @@ describe("short-term dreaming config", () => {
|
|||
const enabled = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
verboseLogging: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -270,6 +275,7 @@ describe("short-term dreaming config", () => {
|
|||
const disabled = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
verboseLogging: "false",
|
||||
},
|
||||
},
|
||||
|
|
@ -283,6 +289,7 @@ describe("short-term dreaming config", () => {
|
|||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
phases: {
|
||||
deep: {
|
||||
minScore: -0.2,
|
||||
|
|
@ -305,15 +312,11 @@ describe("short-term dreaming config", () => {
|
|||
expect(resolved.maxAgeDays).toBe(30);
|
||||
});
|
||||
|
||||
it("keeps deep sleep disabled when the phase is off", () => {
|
||||
it("keeps deep sleep disabled when mode is off", () => {
|
||||
const resolved = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: {
|
||||
dreaming: {
|
||||
phases: {
|
||||
deep: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
mode: "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ describe("memory dreaming host helpers", () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(resolved.mode).toBe("core");
|
||||
expect(resolved.enabled).toBe(true);
|
||||
expect(resolved.timezone).toBe("Europe/London");
|
||||
expect(resolved.storage).toEqual({
|
||||
|
|
@ -85,12 +86,13 @@ describe("memory dreaming host helpers", () => {
|
|||
cfg,
|
||||
});
|
||||
|
||||
expect(resolved.enabled).toBe(true);
|
||||
expect(resolved.mode).toBe("off");
|
||||
expect(resolved.enabled).toBe(false);
|
||||
expect(resolved.timezone).toBe("America/Los_Angeles");
|
||||
expect(resolved.phases.deep).toMatchObject({
|
||||
cron: "0 3 * * *",
|
||||
limit: 10,
|
||||
minScore: 0.8,
|
||||
minScore: 0.75,
|
||||
recencyHalfLifeDays: 14,
|
||||
maxAgeDays: 30,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
|
|||
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export const DEFAULT_MEMORY_DREAMING_ENABLED = true;
|
||||
export const DEFAULT_MEMORY_DREAMING_ENABLED = false;
|
||||
export const DEFAULT_MEMORY_DREAMING_TIMEZONE = undefined;
|
||||
export const DEFAULT_MEMORY_DREAMING_VERBOSE_LOGGING = false;
|
||||
export const DEFAULT_MEMORY_DREAMING_STORAGE_MODE = "inline";
|
||||
export const DEFAULT_MEMORY_DREAMING_SEPARATE_REPORTS = false;
|
||||
export const DEFAULT_MEMORY_DREAMING_MODE = "off";
|
||||
export const DEFAULT_MEMORY_DREAMING_PRESET = "core";
|
||||
|
||||
export const DEFAULT_MEMORY_LIGHT_DREAMING_CRON_EXPR = "0 */6 * * *";
|
||||
export const DEFAULT_MEMORY_LIGHT_DREAMING_LOOKBACK_DAYS = 2;
|
||||
|
|
@ -16,9 +18,9 @@ export const DEFAULT_MEMORY_LIGHT_DREAMING_DEDUPE_SIMILARITY = 0.9;
|
|||
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR = "0 3 * * *";
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_LIMIT = 10;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE = 0.8;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE = 0.75;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT = 3;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES = 3;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES = 2;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS = 14;
|
||||
export const DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS = 30;
|
||||
|
||||
|
|
@ -42,6 +44,8 @@ export type MemoryDreamingSpeed = "fast" | "balanced" | "slow";
|
|||
export type MemoryDreamingThinking = "low" | "medium" | "high";
|
||||
export type MemoryDreamingBudget = "cheap" | "medium" | "expensive";
|
||||
export type MemoryDreamingStorageMode = "inline" | "separate" | "both";
|
||||
export type MemoryDreamingPreset = "core" | "deep" | "rem";
|
||||
export type MemoryDreamingMode = MemoryDreamingPreset | "off";
|
||||
|
||||
export type MemoryLightDreamingSource = "daily" | "sessions" | "recall";
|
||||
export type MemoryDeepDreamingSource = "daily" | "memory" | "sessions" | "logs" | "recall";
|
||||
|
|
@ -108,6 +112,7 @@ export type MemoryRemDreamingConfig = {
|
|||
export type MemoryDreamingPhaseName = "light" | "deep" | "rem";
|
||||
|
||||
export type MemoryDreamingConfig = {
|
||||
mode: MemoryDreamingMode;
|
||||
enabled: boolean;
|
||||
timezone?: string;
|
||||
verboseLogging: boolean;
|
||||
|
|
@ -141,6 +146,47 @@ const DEFAULT_MEMORY_DEEP_DREAMING_SOURCES: MemoryDeepDreamingSource[] = [
|
|||
];
|
||||
const DEFAULT_MEMORY_REM_DREAMING_SOURCES: MemoryRemDreamingSource[] = ["memory", "daily", "deep"];
|
||||
|
||||
const MEMORY_DREAMING_PRESET_DEFAULTS: Record<
|
||||
MemoryDreamingPreset,
|
||||
{
|
||||
cron: string;
|
||||
limit: number;
|
||||
minScore: number;
|
||||
minRecallCount: number;
|
||||
minUniqueQueries: number;
|
||||
recencyHalfLifeDays: number;
|
||||
maxAgeDays: number;
|
||||
}
|
||||
> = {
|
||||
core: {
|
||||
cron: DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR,
|
||||
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
|
||||
minScore: DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE,
|
||||
minRecallCount: DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT,
|
||||
minUniqueQueries: DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES,
|
||||
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
|
||||
},
|
||||
deep: {
|
||||
cron: "0 */12 * * *",
|
||||
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
|
||||
minScore: 0.8,
|
||||
minRecallCount: 3,
|
||||
minUniqueQueries: 3,
|
||||
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
|
||||
},
|
||||
rem: {
|
||||
cron: "0 */6 * * *",
|
||||
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
|
||||
minScore: 0.85,
|
||||
minRecallCount: 4,
|
||||
minUniqueQueries: 3,
|
||||
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
|
||||
},
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
|
|
@ -242,6 +288,23 @@ function normalizeStringArray<T extends string>(
|
|||
return normalized.length > 0 ? normalized : [...fallback];
|
||||
}
|
||||
|
||||
function parseMemoryDreamingMode(value: unknown): MemoryDreamingMode | undefined {
|
||||
const normalized = normalizeTrimmedString(value)?.toLowerCase();
|
||||
if (
|
||||
normalized === "off" ||
|
||||
normalized === "core" ||
|
||||
normalized === "deep" ||
|
||||
normalized === "rem"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeMemoryDreamingMode(value: unknown): MemoryDreamingMode {
|
||||
return parseMemoryDreamingMode(value) ?? DEFAULT_MEMORY_DREAMING_MODE;
|
||||
}
|
||||
|
||||
function normalizeStorageMode(value: unknown): MemoryDreamingStorageMode {
|
||||
const normalized = normalizeTrimmedString(value)?.toLowerCase();
|
||||
if (normalized === "inline" || normalized === "separate" || normalized === "both") {
|
||||
|
|
@ -328,6 +391,13 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
cfg?: OpenClawConfig;
|
||||
}): MemoryDreamingConfig {
|
||||
const dreaming = asRecord(params.pluginConfig?.dreaming);
|
||||
const explicitMode = parseMemoryDreamingMode(dreaming?.mode);
|
||||
const legacyEnabled = normalizeBoolean(dreaming?.enabled, DEFAULT_MEMORY_DREAMING_ENABLED);
|
||||
const mode: MemoryDreamingMode =
|
||||
explicitMode ?? (legacyEnabled ? DEFAULT_MEMORY_DREAMING_PRESET : DEFAULT_MEMORY_DREAMING_MODE);
|
||||
const enabled = mode !== "off";
|
||||
const preset: MemoryDreamingPreset = mode === "off" ? DEFAULT_MEMORY_DREAMING_PRESET : mode;
|
||||
const presetDefaults = MEMORY_DREAMING_PRESET_DEFAULTS[preset];
|
||||
const timezone =
|
||||
normalizeTrimmedString(dreaming?.timezone) ??
|
||||
normalizeTrimmedString(params.cfg?.agents?.defaults?.userTimezone) ??
|
||||
|
|
@ -335,6 +405,7 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
const storage = asRecord(dreaming?.storage);
|
||||
const execution = asRecord(dreaming?.execution);
|
||||
const phases = asRecord(dreaming?.phases);
|
||||
const frequencyAlias = normalizeTrimmedString(dreaming?.frequency);
|
||||
|
||||
const defaultExecution = resolveExecutionConfig(execution?.defaults, {
|
||||
speed: DEFAULT_MEMORY_DREAMING_SPEED,
|
||||
|
|
@ -346,10 +417,39 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
const deep = asRecord(phases?.deep);
|
||||
const rem = asRecord(phases?.rem);
|
||||
const deepRecovery = asRecord(deep?.recovery);
|
||||
const maxAgeDays = normalizeOptionalPositiveInt(deep?.maxAgeDays);
|
||||
const maxAgeDays =
|
||||
normalizeOptionalPositiveInt(dreaming?.maxAgeDays) ??
|
||||
normalizeOptionalPositiveInt(deep?.maxAgeDays) ??
|
||||
presetDefaults.maxAgeDays;
|
||||
const deepCron =
|
||||
normalizeTrimmedString(dreaming?.cron) ??
|
||||
frequencyAlias ??
|
||||
normalizeTrimmedString(deep?.cron) ??
|
||||
presetDefaults.cron;
|
||||
const deepLimit = normalizeNonNegativeInt(
|
||||
dreaming?.limit,
|
||||
normalizeNonNegativeInt(deep?.limit, presetDefaults.limit),
|
||||
);
|
||||
const deepMinScore = normalizeScore(
|
||||
dreaming?.minScore,
|
||||
normalizeScore(deep?.minScore, presetDefaults.minScore),
|
||||
);
|
||||
const deepMinRecallCount = normalizeNonNegativeInt(
|
||||
dreaming?.minRecallCount,
|
||||
normalizeNonNegativeInt(deep?.minRecallCount, presetDefaults.minRecallCount),
|
||||
);
|
||||
const deepMinUniqueQueries = normalizeNonNegativeInt(
|
||||
dreaming?.minUniqueQueries,
|
||||
normalizeNonNegativeInt(deep?.minUniqueQueries, presetDefaults.minUniqueQueries),
|
||||
);
|
||||
const deepRecencyHalfLifeDays = normalizeNonNegativeInt(
|
||||
dreaming?.recencyHalfLifeDays,
|
||||
normalizeNonNegativeInt(deep?.recencyHalfLifeDays, presetDefaults.recencyHalfLifeDays),
|
||||
);
|
||||
|
||||
return {
|
||||
enabled: normalizeBoolean(dreaming?.enabled, DEFAULT_MEMORY_DREAMING_ENABLED),
|
||||
mode,
|
||||
enabled,
|
||||
...(timezone ? { timezone } : {}),
|
||||
verboseLogging: normalizeBoolean(
|
||||
dreaming?.verboseLogging,
|
||||
|
|
@ -367,7 +467,9 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
},
|
||||
phases: {
|
||||
light: {
|
||||
enabled: normalizeBoolean(light?.enabled, true),
|
||||
// Phased dreaming was experimental and too noisy. Keep light sleep off
|
||||
// unless we intentionally bring it back behind a separate effort.
|
||||
enabled: false,
|
||||
cron: normalizeTrimmedString(light?.cron) ?? DEFAULT_MEMORY_LIGHT_DREAMING_CRON_EXPR,
|
||||
lookbackDays: normalizeNonNegativeInt(
|
||||
light?.lookbackDays,
|
||||
|
|
@ -391,27 +493,14 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
}),
|
||||
},
|
||||
deep: {
|
||||
enabled: normalizeBoolean(deep?.enabled, true),
|
||||
cron: normalizeTrimmedString(deep?.cron) ?? DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR,
|
||||
limit: normalizeNonNegativeInt(deep?.limit, DEFAULT_MEMORY_DEEP_DREAMING_LIMIT),
|
||||
minScore: normalizeScore(deep?.minScore, DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE),
|
||||
minRecallCount: normalizeNonNegativeInt(
|
||||
deep?.minRecallCount,
|
||||
DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT,
|
||||
),
|
||||
minUniqueQueries: normalizeNonNegativeInt(
|
||||
deep?.minUniqueQueries,
|
||||
DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES,
|
||||
),
|
||||
recencyHalfLifeDays: normalizeNonNegativeInt(
|
||||
deep?.recencyHalfLifeDays,
|
||||
DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
),
|
||||
...(typeof maxAgeDays === "number"
|
||||
? { maxAgeDays }
|
||||
: typeof DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS === "number"
|
||||
? { maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS }
|
||||
: {}),
|
||||
enabled,
|
||||
cron: deepCron,
|
||||
limit: deepLimit,
|
||||
minScore: deepMinScore,
|
||||
minRecallCount: deepMinRecallCount,
|
||||
minUniqueQueries: deepMinUniqueQueries,
|
||||
recencyHalfLifeDays: deepRecencyHalfLifeDays,
|
||||
...(typeof maxAgeDays === "number" ? { maxAgeDays } : {}),
|
||||
sources: normalizeStringArray(
|
||||
deep?.sources,
|
||||
["daily", "memory", "sessions", "logs", "recall"] as const,
|
||||
|
|
@ -451,7 +540,9 @@ export function resolveMemoryDreamingConfig(params: {
|
|||
}),
|
||||
},
|
||||
rem: {
|
||||
enabled: normalizeBoolean(rem?.enabled, true),
|
||||
// REM reflections are currently disabled by default to keep dreaming
|
||||
// predictable and focused on durable promotion modes.
|
||||
enabled: false,
|
||||
cron: normalizeTrimmedString(rem?.cron) ?? DEFAULT_MEMORY_REM_DREAMING_CRON_EXPR,
|
||||
lookbackDays: normalizeNonNegativeInt(
|
||||
rem?.lookbackDays,
|
||||
|
|
|
|||
|
|
@ -66,9 +66,8 @@ import {
|
|||
} from "./controllers/devices.ts";
|
||||
import {
|
||||
loadDreamingStatus,
|
||||
updateDreamingEnabled,
|
||||
updateDreamingPhaseEnabled,
|
||||
type DreamingPhaseId,
|
||||
updateDreamingMode,
|
||||
type DreamingMode,
|
||||
} from "./controllers/dreaming.ts";
|
||||
import {
|
||||
loadExecApprovals,
|
||||
|
|
@ -150,42 +149,34 @@ const lazyNodes = createLazy(() => import("./views/nodes.ts"));
|
|||
const lazySessions = createLazy(() => import("./views/sessions.ts"));
|
||||
const lazySkills = createLazy(() => import("./views/skills.ts"));
|
||||
const lazyDreamingView = createLazy(() => import("./views/dreaming.ts"));
|
||||
const DREAMING_PHASE_OPTIONS: Array<{ id: DreamingPhaseId; label: string; detail: string }> = [
|
||||
{ id: "light", label: "Light", detail: "sort and stage the day" },
|
||||
{ id: "deep", label: "Deep", detail: "promote durable memory" },
|
||||
{ id: "rem", label: "REM", detail: "surface themes and reflections" },
|
||||
const DREAMING_MODE_OPTIONS: Array<{ id: DreamingMode; label: string; detail: string }> = [
|
||||
{ id: "off", label: "Off", detail: "no automatic promotion" },
|
||||
{ id: "core", label: "Core", detail: "nightly durable consolidation" },
|
||||
{ id: "rem", label: "REM", detail: "every 6 hours, stricter" },
|
||||
{ id: "deep", label: "Deep", detail: "every 12 hours, conservative" },
|
||||
];
|
||||
|
||||
function resolveConfiguredDreaming(configValue: Record<string, unknown> | null): {
|
||||
enabled: boolean;
|
||||
phases: Record<DreamingPhaseId, boolean>;
|
||||
mode: DreamingMode;
|
||||
} {
|
||||
const fallback: DreamingMode = "off";
|
||||
if (!configValue) {
|
||||
return {
|
||||
enabled: true,
|
||||
phases: {
|
||||
light: true,
|
||||
deep: true,
|
||||
rem: true,
|
||||
},
|
||||
};
|
||||
return { mode: fallback };
|
||||
}
|
||||
const plugins = configValue.plugins as Record<string, unknown> | undefined;
|
||||
const entries = plugins?.entries as Record<string, unknown> | undefined;
|
||||
const memoryCore = entries?.["memory-core"] as Record<string, unknown> | undefined;
|
||||
const config = memoryCore?.config as Record<string, unknown> | undefined;
|
||||
const dreaming = config?.dreaming as Record<string, unknown> | undefined;
|
||||
const phases = dreaming?.phases as Record<string, unknown> | undefined;
|
||||
const light = phases?.light as Record<string, unknown> | undefined;
|
||||
const deep = phases?.deep as Record<string, unknown> | undefined;
|
||||
const rem = phases?.rem as Record<string, unknown> | undefined;
|
||||
const modeRaw = typeof dreaming?.mode === "string" ? dreaming.mode.trim().toLowerCase() : "";
|
||||
if (modeRaw === "off" || modeRaw === "core" || modeRaw === "rem" || modeRaw === "deep") {
|
||||
return { mode: modeRaw };
|
||||
}
|
||||
if (typeof dreaming?.enabled === "boolean") {
|
||||
return { mode: dreaming.enabled ? "core" : "off" };
|
||||
}
|
||||
return {
|
||||
enabled: typeof dreaming?.enabled === "boolean" ? dreaming.enabled : true,
|
||||
phases: {
|
||||
light: typeof light?.enabled === "boolean" ? light.enabled : true,
|
||||
deep: typeof deep?.enabled === "boolean" ? deep.enabled : true,
|
||||
rem: typeof rem?.enabled === "boolean" ? rem.enabled : true,
|
||||
},
|
||||
mode: fallback,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -200,12 +191,20 @@ function formatDreamNextCycle(nextRunAtMs: number | undefined): string | null {
|
|||
}
|
||||
|
||||
function resolveDreamingNextCycle(
|
||||
status: { phases: Record<DreamingPhaseId, { enabled: boolean; nextRunAtMs?: number }> } | null,
|
||||
status: {
|
||||
phases: {
|
||||
deep?: { enabled: boolean; nextRunAtMs?: number };
|
||||
light?: { enabled: boolean; nextRunAtMs?: number };
|
||||
rem?: { enabled: boolean; nextRunAtMs?: number };
|
||||
};
|
||||
} | null,
|
||||
): string | null {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
const nextRunAtMs = Object.values(status.phases)
|
||||
const phases = [status.phases.deep, status.phases.light, status.phases.rem];
|
||||
const nextRunAtMs = phases
|
||||
.filter((phase): phase is { enabled: boolean; nextRunAtMs?: number } => Boolean(phase))
|
||||
.filter((phase) => phase.enabled && typeof phase.nextRunAtMs === "number")
|
||||
.map((phase) => phase.nextRunAtMs as number)
|
||||
.toSorted((a, b) => a - b)[0];
|
||||
|
|
@ -398,34 +397,17 @@ export function renderApp(state: AppViewState) {
|
|||
const configValue =
|
||||
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
|
||||
const configuredDreaming = resolveConfiguredDreaming(configValue);
|
||||
const dreamingOn = state.dreamingStatus?.enabled ?? configuredDreaming.enabled;
|
||||
const dreamingMode = configuredDreaming.mode;
|
||||
const dreamingOn = dreamingMode !== "off";
|
||||
const dreamingNextCycle = resolveDreamingNextCycle(state.dreamingStatus);
|
||||
const dreamingLoading = state.dreamingStatusLoading || state.dreamingModeSaving;
|
||||
const refreshDreamingStatus = () => loadDreamingStatus(state);
|
||||
const applyDreamingEnabled = (enabled: boolean) => {
|
||||
if (state.dreamingModeSaving || dreamingOn === enabled) {
|
||||
const applyDreamingMode = (mode: DreamingMode) => {
|
||||
if (state.dreamingModeSaving || dreamingMode === mode) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
const updated = await updateDreamingEnabled(state, enabled);
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
await loadConfig(state);
|
||||
await loadDreamingStatus(state);
|
||||
})();
|
||||
};
|
||||
const applyDreamingPhaseEnabled = (phase: DreamingPhaseId, enabled: boolean) => {
|
||||
if (state.dreamingModeSaving) {
|
||||
return;
|
||||
}
|
||||
const currentEnabled =
|
||||
state.dreamingStatus?.phases[phase].enabled ?? configuredDreaming.phases[phase];
|
||||
if (currentEnabled === enabled) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
const updated = await updateDreamingPhaseEnabled(state, phase, enabled);
|
||||
const updated = await updateDreamingMode(state, mode);
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -740,24 +722,28 @@ export function renderApp(state: AppViewState) {
|
|||
<div
|
||||
class="dreaming-header-controls__modes"
|
||||
role="group"
|
||||
aria-label="Dreaming controls"
|
||||
aria-label="Dreaming mode"
|
||||
>
|
||||
<button
|
||||
class="dreaming-header-controls__mode ${dreamingOn
|
||||
? "dreaming-header-controls__mode--active"
|
||||
: ""}"
|
||||
?disabled=${dreamingLoading}
|
||||
title=${dreamingOn ? "Dreaming is enabled." : "Dreaming is disabled."}
|
||||
aria-label=${dreamingOn ? "Disable dreaming" : "Enable dreaming"}
|
||||
@click=${() => applyDreamingEnabled(!dreamingOn)}
|
||||
>
|
||||
<span class="dreaming-header-controls__mode-label"
|
||||
>${dreamingOn ? "Dreaming On" : "Dreaming Off"}</span
|
||||
>
|
||||
<span class="dreaming-header-controls__mode-detail"
|
||||
>${dreamingOn ? "all phases may run" : "no phases will run"}</span
|
||||
>
|
||||
</button>
|
||||
${DREAMING_MODE_OPTIONS.map(
|
||||
(option) => html`
|
||||
<button
|
||||
class="dreaming-header-controls__mode ${dreamingMode === option.id
|
||||
? "dreaming-header-controls__mode--active"
|
||||
: ""}"
|
||||
?disabled=${dreamingLoading}
|
||||
title=${option.detail}
|
||||
aria-label=${`Set dreaming mode to ${option.label}`}
|
||||
@click=${() => applyDreamingMode(option.id)}
|
||||
>
|
||||
<span class="dreaming-header-controls__mode-label"
|
||||
>${option.label}</span
|
||||
>
|
||||
<span class="dreaming-header-controls__mode-detail"
|
||||
>${option.detail}</span
|
||||
>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
@ -2148,29 +2134,19 @@ export function renderApp(state: AppViewState) {
|
|||
? lazyRender(lazyDreamingView, (m) =>
|
||||
m.renderDreaming({
|
||||
active: dreamingOn,
|
||||
mode: dreamingMode,
|
||||
shortTermCount: state.dreamingStatus?.shortTermCount ?? 0,
|
||||
longTermCount: state.dreamingStatus?.promotedTotal ?? 0,
|
||||
promotedCount: state.dreamingStatus?.promotedToday ?? 0,
|
||||
dreamingOf: null,
|
||||
nextCycle: dreamingNextCycle,
|
||||
timezone: state.dreamingStatus?.timezone ?? null,
|
||||
phases: DREAMING_PHASE_OPTIONS.map((phase) => ({
|
||||
...phase,
|
||||
enabled:
|
||||
state.dreamingStatus?.phases[phase.id].enabled ??
|
||||
configuredDreaming.phases[phase.id],
|
||||
nextCycle: formatDreamNextCycle(
|
||||
state.dreamingStatus?.phases[phase.id].nextRunAtMs,
|
||||
),
|
||||
managedCronPresent:
|
||||
state.dreamingStatus?.phases[phase.id].managedCronPresent ?? false,
|
||||
})),
|
||||
modes: DREAMING_MODE_OPTIONS,
|
||||
statusLoading: state.dreamingStatusLoading,
|
||||
statusError: state.dreamingStatusError,
|
||||
modeSaving: state.dreamingModeSaving,
|
||||
onRefresh: refreshDreamingStatus,
|
||||
onToggleEnabled: applyDreamingEnabled,
|
||||
onTogglePhase: applyDreamingPhaseEnabled,
|
||||
onSelectMode: applyDreamingMode,
|
||||
}),
|
||||
)
|
||||
: nothing}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
loadDreamingStatus,
|
||||
updateDreamingEnabled,
|
||||
updateDreamingPhaseEnabled,
|
||||
type DreamingState,
|
||||
} from "./dreaming.ts";
|
||||
import { loadDreamingStatus, updateDreamingMode, type DreamingState } from "./dreaming.ts";
|
||||
|
||||
function createState(): { state: DreamingState; request: ReturnType<typeof vi.fn> } {
|
||||
const request = vi.fn();
|
||||
|
|
@ -29,6 +24,7 @@ describe("dreaming controller", () => {
|
|||
const { state, request } = createState();
|
||||
request.mockResolvedValue({
|
||||
dreaming: {
|
||||
mode: "core",
|
||||
enabled: true,
|
||||
timezone: "America/Los_Angeles",
|
||||
verboseLogging: false,
|
||||
|
|
@ -76,6 +72,7 @@ describe("dreaming controller", () => {
|
|||
expect(request).toHaveBeenCalledWith("doctor.memory.status", {});
|
||||
expect(state.dreamingStatus).toEqual(
|
||||
expect.objectContaining({
|
||||
mode: "core",
|
||||
enabled: true,
|
||||
shortTermCount: 8,
|
||||
promotedToday: 2,
|
||||
|
|
@ -91,17 +88,18 @@ describe("dreaming controller", () => {
|
|||
expect(state.dreamingStatusError).toBeNull();
|
||||
});
|
||||
|
||||
it("patches config to update global dreaming enablement", async () => {
|
||||
it("patches config to update dreaming mode", async () => {
|
||||
const { state, request } = createState();
|
||||
request.mockResolvedValue({ ok: true });
|
||||
|
||||
const ok = await updateDreamingEnabled(state, false);
|
||||
const ok = await updateDreamingMode(state, "deep");
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"config.patch",
|
||||
expect.objectContaining({
|
||||
baseHash: "hash-1",
|
||||
raw: expect.stringContaining('"mode":"deep"'),
|
||||
sessionKey: "main",
|
||||
}),
|
||||
);
|
||||
|
|
@ -109,26 +107,11 @@ describe("dreaming controller", () => {
|
|||
expect(state.dreamingStatusError).toBeNull();
|
||||
});
|
||||
|
||||
it("patches config to update phase enablement", async () => {
|
||||
const { state, request } = createState();
|
||||
request.mockResolvedValue({ ok: true });
|
||||
|
||||
const ok = await updateDreamingPhaseEnabled(state, "rem", false);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"config.patch",
|
||||
expect.objectContaining({
|
||||
raw: expect.stringContaining('"rem":{"enabled":false}'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails gracefully when config hash is missing", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {};
|
||||
|
||||
const ok = await updateDreamingEnabled(state, true);
|
||||
const ok = await updateDreamingMode(state, "core");
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import type { ConfigSnapshot } from "../types.ts";
|
||||
|
||||
export type DreamingPhaseId = "light" | "deep" | "rem";
|
||||
export type DreamingMode = "off" | "core" | "rem" | "deep";
|
||||
|
||||
type DreamingPhaseStatusBase = {
|
||||
enabled: boolean;
|
||||
|
|
@ -31,6 +31,7 @@ type RemDreamingStatus = DreamingPhaseStatusBase & {
|
|||
};
|
||||
|
||||
export type DreamingStatus = {
|
||||
mode: DreamingMode;
|
||||
enabled: boolean;
|
||||
timezone?: string;
|
||||
verboseLogging: boolean;
|
||||
|
|
@ -105,6 +106,19 @@ function normalizeStorageMode(value: unknown): DreamingStatus["storageMode"] {
|
|||
return "inline";
|
||||
}
|
||||
|
||||
function normalizeDreamingMode(value: unknown): DreamingMode | undefined {
|
||||
const normalized = normalizeTrimmedString(value)?.toLowerCase();
|
||||
if (
|
||||
normalized === "off" ||
|
||||
normalized === "core" ||
|
||||
normalized === "rem" ||
|
||||
normalized === "deep"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeNextRun(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
|
@ -132,9 +146,13 @@ function normalizeDreamingStatus(raw: unknown): DreamingStatus | null {
|
|||
const timezone = normalizeTrimmedString(record.timezone);
|
||||
const storePath = normalizeTrimmedString(record.storePath);
|
||||
const storeError = normalizeTrimmedString(record.storeError);
|
||||
const explicitMode = normalizeDreamingMode(record.mode);
|
||||
const enabled = normalizeBoolean(record.enabled, false);
|
||||
const mode = explicitMode ?? (enabled ? "core" : "off");
|
||||
|
||||
return {
|
||||
enabled: normalizeBoolean(record.enabled, false),
|
||||
mode,
|
||||
enabled,
|
||||
...(timezone ? { timezone } : {}),
|
||||
verboseLogging: normalizeBoolean(record.verboseLogging, false),
|
||||
storageMode: normalizeStorageMode(record.storageMode),
|
||||
|
|
@ -230,32 +248,12 @@ export async function updateDreamingEnabled(
|
|||
state: DreamingState,
|
||||
enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
const ok = await writeDreamingPatch(state, {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (ok && state.dreamingStatus) {
|
||||
state.dreamingStatus = {
|
||||
...state.dreamingStatus,
|
||||
enabled,
|
||||
};
|
||||
}
|
||||
return ok;
|
||||
return updateDreamingMode(state, enabled ? "core" : "off");
|
||||
}
|
||||
|
||||
export async function updateDreamingPhaseEnabled(
|
||||
export async function updateDreamingMode(
|
||||
state: DreamingState,
|
||||
phase: DreamingPhaseId,
|
||||
enabled: boolean,
|
||||
mode: DreamingMode,
|
||||
): Promise<boolean> {
|
||||
const ok = await writeDreamingPatch(state, {
|
||||
plugins: {
|
||||
|
|
@ -263,11 +261,7 @@ export async function updateDreamingPhaseEnabled(
|
|||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
phases: {
|
||||
[phase]: {
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -277,13 +271,8 @@ export async function updateDreamingPhaseEnabled(
|
|||
if (ok && state.dreamingStatus) {
|
||||
state.dreamingStatus = {
|
||||
...state.dreamingStatus,
|
||||
phases: {
|
||||
...state.dreamingStatus.phases,
|
||||
[phase]: {
|
||||
...state.dreamingStatus.phases[phase],
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
mode,
|
||||
enabled: mode !== "off",
|
||||
};
|
||||
}
|
||||
return ok;
|
||||
|
|
|
|||
|
|
@ -7,44 +7,40 @@ import { renderDreaming, type DreamingProps } from "./dreaming.ts";
|
|||
function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
|
||||
return {
|
||||
active: true,
|
||||
mode: "core",
|
||||
shortTermCount: 47,
|
||||
longTermCount: 182,
|
||||
promotedCount: 12,
|
||||
dreamingOf: null,
|
||||
nextCycle: "4:00 AM",
|
||||
timezone: "America/Los_Angeles",
|
||||
phases: [
|
||||
modes: [
|
||||
{
|
||||
id: "light",
|
||||
label: "Light",
|
||||
detail: "sort and stage the day",
|
||||
enabled: true,
|
||||
nextCycle: "1:00 AM",
|
||||
managedCronPresent: true,
|
||||
id: "off",
|
||||
label: "Off",
|
||||
detail: "no automatic promotion",
|
||||
},
|
||||
{
|
||||
id: "core",
|
||||
label: "Core",
|
||||
detail: "nightly durable consolidation",
|
||||
},
|
||||
{
|
||||
id: "deep",
|
||||
label: "Deep",
|
||||
detail: "promote durable memory",
|
||||
enabled: true,
|
||||
nextCycle: "3:00 AM",
|
||||
managedCronPresent: true,
|
||||
detail: "every 12 hours, conservative",
|
||||
},
|
||||
{
|
||||
id: "rem",
|
||||
label: "REM",
|
||||
detail: "surface themes and reflections",
|
||||
enabled: false,
|
||||
nextCycle: null,
|
||||
managedCronPresent: false,
|
||||
detail: "every 6 hours, stricter",
|
||||
},
|
||||
],
|
||||
statusLoading: false,
|
||||
statusError: null,
|
||||
modeSaving: false,
|
||||
onRefresh: () => {},
|
||||
onToggleEnabled: () => {},
|
||||
onTogglePhase: () => {},
|
||||
onSelectMode: () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -107,13 +103,13 @@ describe("dreaming view", () => {
|
|||
it("shows active status label when active", () => {
|
||||
const container = renderInto(buildProps({ active: true }));
|
||||
const label = container.querySelector(".dreams__status-label");
|
||||
expect(label?.textContent).toBe("Dreaming Active");
|
||||
expect(label?.textContent).toContain("Dreaming Active");
|
||||
});
|
||||
|
||||
it("shows idle status label when inactive", () => {
|
||||
const container = renderInto(buildProps({ active: false }));
|
||||
const label = container.querySelector(".dreams__status-label");
|
||||
expect(label?.textContent).toBe("Dreaming Idle");
|
||||
expect(label?.textContent).toContain("Dreaming Idle");
|
||||
});
|
||||
|
||||
it("applies idle class when not active", () => {
|
||||
|
|
@ -127,10 +123,10 @@ describe("dreaming view", () => {
|
|||
expect(detail?.textContent).toContain("4:00 AM");
|
||||
});
|
||||
|
||||
it("renders phase controls", () => {
|
||||
it("renders mode controls", () => {
|
||||
const container = renderInto(buildProps());
|
||||
expect(container.querySelector(".dreams__controls")).not.toBeNull();
|
||||
expect(container.querySelectorAll(".dreams__phase").length).toBe(3);
|
||||
expect(container.querySelectorAll(".dreams__phase").length).toBe(4);
|
||||
});
|
||||
|
||||
it("renders control error when present", () => {
|
||||
|
|
@ -140,12 +136,12 @@ describe("dreaming view", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("wires phase toggle callbacks", () => {
|
||||
const onTogglePhase = vi.fn();
|
||||
const container = renderInto(buildProps({ onTogglePhase }));
|
||||
it("wires mode selection callbacks", () => {
|
||||
const onSelectMode = vi.fn();
|
||||
const container = renderInto(buildProps({ onSelectMode }));
|
||||
|
||||
container.querySelector<HTMLButtonElement>(".dreams__phase .btn")?.click();
|
||||
|
||||
expect(onTogglePhase).toHaveBeenCalled();
|
||||
expect(onSelectMode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,25 @@
|
|||
import { html, nothing } from "lit";
|
||||
import type { DreamingPhaseId } from "../controllers/dreaming.ts";
|
||||
import type { DreamingMode } from "../controllers/dreaming.ts";
|
||||
|
||||
export type DreamingProps = {
|
||||
active: boolean;
|
||||
mode: DreamingMode;
|
||||
shortTermCount: number;
|
||||
longTermCount: number;
|
||||
promotedCount: number;
|
||||
dreamingOf: string | null;
|
||||
nextCycle: string | null;
|
||||
timezone: string | null;
|
||||
phases: Array<{
|
||||
id: DreamingPhaseId;
|
||||
modes: Array<{
|
||||
id: DreamingMode;
|
||||
label: string;
|
||||
detail: string;
|
||||
enabled: boolean;
|
||||
nextCycle: string | null;
|
||||
managedCronPresent: boolean;
|
||||
}>;
|
||||
statusLoading: boolean;
|
||||
statusError: string | null;
|
||||
modeSaving: boolean;
|
||||
onRefresh: () => void;
|
||||
onToggleEnabled: (enabled: boolean) => void;
|
||||
onTogglePhase: (phase: DreamingPhaseId, enabled: boolean) => void;
|
||||
onSelectMode: (mode: DreamingMode) => void;
|
||||
};
|
||||
|
||||
const DREAM_PHRASES = [
|
||||
|
|
@ -163,13 +160,13 @@ export function renderDreaming(props: DreamingProps) {
|
|||
|
||||
<div class="dreams__status">
|
||||
<span class="dreams__status-label"
|
||||
>${props.active ? "Dreaming Active" : "Dreaming Idle"}</span
|
||||
>${props.active ? "Dreaming Active" : "Dreaming Idle"} · ${props.mode.toUpperCase()}</span
|
||||
>
|
||||
<div class="dreams__status-detail">
|
||||
<div class="dreams__status-dot"></div>
|
||||
<span>
|
||||
${props.promotedCount} promoted
|
||||
${props.nextCycle ? html`· next phase ${props.nextCycle}` : nothing}
|
||||
${props.nextCycle ? html`· next run ${props.nextCycle}` : nothing}
|
||||
${props.timezone ? html`· ${props.timezone}` : nothing}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -201,9 +198,9 @@ export function renderDreaming(props: DreamingProps) {
|
|||
<div class="dreams__controls">
|
||||
<div class="dreams__controls-head">
|
||||
<div>
|
||||
<div class="dreams__controls-title">Dreaming Phases</div>
|
||||
<div class="dreams__controls-title">Dreaming Modes</div>
|
||||
<div class="dreams__controls-subtitle">
|
||||
Light sleep sorts, deep sleep keeps, REM reflects.
|
||||
Pick a cadence and threshold profile for durable promotion.
|
||||
</div>
|
||||
</div>
|
||||
<div class="dreams__controls-actions">
|
||||
|
|
@ -214,36 +211,38 @@ export function renderDreaming(props: DreamingProps) {
|
|||
>
|
||||
${props.statusLoading ? "Refreshing…" : "Refresh"}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm ${props.active ? "btn--subtle" : ""}"
|
||||
?disabled=${props.modeSaving}
|
||||
@click=${() => props.onToggleEnabled(!props.active)}
|
||||
>
|
||||
${props.active ? "Disable Dreaming" : "Enable Dreaming"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dreams__phase-grid">
|
||||
${props.phases.map(
|
||||
(phase) => html`
|
||||
<article class="dreams__phase ${phase.enabled ? "dreams__phase--active" : ""}">
|
||||
${props.modes.map(
|
||||
(mode) => html`
|
||||
<article
|
||||
class="dreams__phase ${props.mode === mode.id ? "dreams__phase--active" : ""}"
|
||||
>
|
||||
<div class="dreams__phase-top">
|
||||
<div>
|
||||
<div class="dreams__phase-label">${phase.label}</div>
|
||||
<div class="dreams__phase-detail">${phase.detail}</div>
|
||||
<div class="dreams__phase-label">${mode.label}</div>
|
||||
<div class="dreams__phase-detail">${mode.detail}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn--subtle btn--sm"
|
||||
?disabled=${props.modeSaving || !props.active}
|
||||
@click=${() => props.onTogglePhase(phase.id, !phase.enabled)}
|
||||
?disabled=${props.modeSaving || props.mode === mode.id}
|
||||
@click=${() => props.onSelectMode(mode.id)}
|
||||
>
|
||||
${phase.enabled ? "Pause" : "Enable"}
|
||||
${props.mode === mode.id ? "Active" : "Set"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="dreams__phase-meta">
|
||||
<span>${phase.enabled ? "scheduled" : "off"}</span>
|
||||
<span>${phase.nextCycle ? `next ${phase.nextCycle}` : "no next run"}</span>
|
||||
<span>${phase.managedCronPresent ? "managed cron" : "cron missing"}</span>
|
||||
<span>${props.mode === mode.id ? "selected" : "available"}</span>
|
||||
<span>
|
||||
${mode.id === "off"
|
||||
? "no background runs"
|
||||
: mode.id === "core"
|
||||
? "nightly cadence"
|
||||
: mode.id === "deep"
|
||||
? "every 12 hours"
|
||||
: "every 6 hours"}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue