fix(cron): prevent cross-agent job leakage from global store migration (#588)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Cameron
2026-03-12 22:53:20 -07:00
committed by GitHub
parent b0fc98d064
commit 25fbd7b9fa

View File

@@ -6,7 +6,7 @@
*/
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, copyFileSync, renameSync, watch, type FSWatcher } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { resolve, dirname, basename } from 'node:path';
import type { AgentSession } from '../core/interfaces.js';
import type { CronJob, CronJobCreate, CronSchedule, CronConfig, HeartbeatConfig } from './types.js';
import { DEFAULT_HEARTBEAT_MESSAGES } from './types.js';
@@ -101,11 +101,38 @@ export class CronService {
if (globalPath === this.storePath || !existsSync(globalPath)) return;
try {
// Filter jobs: only adopt those whose delivery channels exist on this agent
const agentChannels = new Set(this.bot.getStatus().channels);
const globalData: CronStoreFile = JSON.parse(readFileSync(globalPath, 'utf-8'));
const adopted: typeof globalData.jobs = [];
const skipped: string[] = [];
for (const job of globalData.jobs) {
const deliverChannel = job.deliver?.channel;
if (deliverChannel && !agentChannels.has(deliverChannel)) {
skipped.push(`${job.id} (channel=${deliverChannel})`);
continue;
}
adopted.push(job);
}
if (skipped.length > 0) {
log.warn(`Global store migration: skipped ${skipped.length} job(s) with unavailable channels: ${skipped.join(', ')}`);
}
if (adopted.length > 0) {
mkdirSync(dirname(this.storePath), { recursive: true });
copyFileSync(globalPath, this.storePath);
writeFileSync(this.storePath, JSON.stringify({ ...globalData, jobs: adopted }, null, 2));
logEvent('store_migrated_from_global', {
from: globalPath,
to: this.storePath,
adopted: adopted.length,
skipped: skipped.length,
});
}
// Rename global file so subsequent agents don't also copy it
renameSync(globalPath, globalPath + '.migrated');
logEvent('store_migrated_from_global', { from: globalPath, to: this.storePath });
} catch (e) {
log.error('Failed to migrate from global cron store:', e);
}
@@ -224,8 +251,9 @@ export class CronService {
} catch {
// File might not exist yet, watch the directory instead
const dir = resolve(this.storePath, '..');
const storeBasename = basename(this.storePath);
this.fileWatcher = watch(dir, { persistent: false }, (eventType, filename) => {
if (filename === 'cron-jobs.json') {
if (filename === storeBasename) {
this.handleFileChange();
}
});