49758568bb
Start a periodic reflect loop from portal and MindSpace service bootstraps when EXPERIENCE_REFLECT_ENABLED=1, aggregating repeated task outcomes on a configurable interval. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.4 KiB
JavaScript
131 lines
3.4 KiB
JavaScript
import { DEFAULT_REFLECT_MIN_GROUP_SIZE } from './experience-reflect.mjs';
|
|
|
|
function readFlag(value, fallback = false) {
|
|
if (value == null || value === '') return fallback;
|
|
const normalized = String(value).trim().toLowerCase();
|
|
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
return fallback;
|
|
}
|
|
|
|
function bounded(value, fallback, min, max) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.min(max, Math.max(min, parsed));
|
|
}
|
|
|
|
export function resolveExperienceReflectPolicy(env = process.env) {
|
|
const intervalHours = Math.round(
|
|
bounded(env.EXPERIENCE_REFLECT_INTERVAL_HOURS, 6, 1, 168),
|
|
);
|
|
return {
|
|
enabled: readFlag(env.EXPERIENCE_REFLECT_ENABLED, false),
|
|
intervalMs: intervalHours * 3_600_000,
|
|
minGroupSize: Math.round(
|
|
bounded(env.EXPERIENCE_REFLECT_MIN_GROUP_SIZE, DEFAULT_REFLECT_MIN_GROUP_SIZE, 2, 20),
|
|
),
|
|
scopes: String(env.EXPERIENCE_REFLECT_SCOPES ?? 'global')
|
|
.split(/[\s,]+/u)
|
|
.map((scope) => scope.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 20),
|
|
runOnStart: readFlag(env.EXPERIENCE_REFLECT_RUN_ON_START, true),
|
|
};
|
|
}
|
|
|
|
export async function runExperienceReflectOnce({
|
|
experienceService,
|
|
policy = resolveExperienceReflectPolicy(),
|
|
logger = console,
|
|
} = {}) {
|
|
if (!experienceService || typeof experienceService.reflect !== 'function') {
|
|
return { ok: false, skipped: true, reason: 'experience_service_unavailable' };
|
|
}
|
|
if (!policy.enabled) {
|
|
return { ok: true, skipped: true, reason: 'disabled' };
|
|
}
|
|
|
|
const scopes = policy.scopes.length > 0 ? policy.scopes : ['global'];
|
|
const results = [];
|
|
let created = 0;
|
|
let archived = 0;
|
|
|
|
for (const scope of scopes) {
|
|
const result = await experienceService.reflect({
|
|
scope,
|
|
minGroupSize: policy.minGroupSize,
|
|
});
|
|
results.push({ scope, ...result });
|
|
created += Number(result?.created ?? 0);
|
|
archived += Number(result?.archived ?? 0);
|
|
}
|
|
|
|
if (created > 0 || archived > 0) {
|
|
logger.log?.(
|
|
`[ExperienceReflect] created=${created} archived=${archived} scopes=${scopes.join(',')}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
skipped: false,
|
|
created,
|
|
archived,
|
|
scopes,
|
|
results,
|
|
};
|
|
}
|
|
|
|
export function startExperienceReflectWorker({
|
|
experienceService,
|
|
env = process.env,
|
|
logger = console,
|
|
setIntervalFn = setInterval,
|
|
} = {}) {
|
|
const policy = resolveExperienceReflectPolicy(env);
|
|
if (!policy.enabled || !experienceService?.reflect) {
|
|
return { stop() {}, policy };
|
|
}
|
|
|
|
let stopped = false;
|
|
let running = false;
|
|
|
|
const runOnce = async () => {
|
|
if (stopped || running) return;
|
|
running = true;
|
|
try {
|
|
await runExperienceReflectOnce({
|
|
experienceService,
|
|
policy,
|
|
logger,
|
|
});
|
|
} catch (error) {
|
|
logger.warn?.(
|
|
'[ExperienceReflect] worker run failed:',
|
|
error instanceof Error ? error.message : error,
|
|
);
|
|
} finally {
|
|
running = false;
|
|
}
|
|
};
|
|
|
|
if (policy.runOnStart) {
|
|
void runOnce();
|
|
}
|
|
|
|
const timer = setIntervalFn(runOnce, policy.intervalMs);
|
|
timer?.unref?.();
|
|
logger.log?.(
|
|
`[ExperienceReflect] worker enabled (interval=${policy.intervalMs}ms, minGroup=${policy.minGroupSize}, scopes=${policy.scopes.join(',') || 'global'})`,
|
|
);
|
|
|
|
return {
|
|
policy,
|
|
stop() {
|
|
stopped = true;
|
|
clearInterval(timer);
|
|
},
|
|
runOnce,
|
|
};
|
|
}
|