feat(FN-1399): add background memory summarization after task completion
- Add MemoryInsights class in @fusion/core for AI-powered memory audit generation - Add post-run hook to CronRunner for triggering memory summarization after scheduled tasks - Wire memory background processing in both dashboard and serve commands - Add memoryAuditEnabled and memoryAuditSchedule settings for configurable automation - Fix startup ordering: sync automation before cronRunner.start() to prevent race conditions - Add comprehensive tests for memory-insights and dashboard/serve integration - Update contributing.md and settings-reference.md with documentation
This commit is contained in:
@@ -276,6 +276,120 @@ describe("CronRunner", () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("err");
|
||||
});
|
||||
|
||||
it("calls recordRun before callback on success", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "echo success" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
const callOrder: string[] = [];
|
||||
const onProcessed = vi.fn().mockImplementation(() => {
|
||||
callOrder.push("callback");
|
||||
});
|
||||
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
await runner.executeSchedule(schedule);
|
||||
|
||||
expect(callOrder).toContain("callback");
|
||||
// recordRun should have been called first
|
||||
expect(automationStore.recordRun).toHaveBeenCalledBefore(onProcessed);
|
||||
});
|
||||
|
||||
it("invokes callback on successful execution", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "echo success" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
const onProcessed = vi.fn();
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
|
||||
await runner.executeSchedule(schedule);
|
||||
|
||||
expect(onProcessed).toHaveBeenCalledTimes(1);
|
||||
expect(onProcessed).toHaveBeenCalledWith(
|
||||
schedule,
|
||||
expect.objectContaining({ success: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes callback on failed execution", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "exit 1" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
const onProcessed = vi.fn();
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
|
||||
await runner.executeSchedule(schedule);
|
||||
|
||||
expect(onProcessed).toHaveBeenCalledTimes(1);
|
||||
expect(onProcessed).toHaveBeenCalledWith(
|
||||
schedule,
|
||||
expect.objectContaining({ success: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not invoke callback when not provided", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "echo test" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
runner = new CronRunner(store, automationStore);
|
||||
|
||||
// Should not throw
|
||||
await runner.executeSchedule(schedule);
|
||||
});
|
||||
|
||||
it("callback errors do not throw or alter returned result", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "echo success" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
const onProcessed = vi.fn().mockRejectedValue(new Error("callback failed"));
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
|
||||
// Should not throw
|
||||
const result = await runner.executeSchedule(schedule);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("success");
|
||||
});
|
||||
|
||||
it("callback errors do not prevent recordRun", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({ command: "echo test" });
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
const onProcessed = vi.fn().mockRejectedValue(new Error("callback failed"));
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
|
||||
await runner.executeSchedule(schedule);
|
||||
|
||||
expect(automationStore.recordRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes schedule and result to callback", async () => {
|
||||
const store = createMockStore();
|
||||
const schedule = createMockSchedule({
|
||||
id: "custom-id",
|
||||
name: "Custom Schedule",
|
||||
command: "echo custom",
|
||||
});
|
||||
const automationStore = createMockAutomationStore([schedule]);
|
||||
|
||||
let receivedSchedule: ScheduledTask | undefined;
|
||||
let receivedResult: AutomationRunResult | undefined;
|
||||
const onProcessed = vi.fn().mockImplementation((s: ScheduledTask, r: AutomationRunResult) => {
|
||||
receivedSchedule = s;
|
||||
receivedResult = r;
|
||||
});
|
||||
|
||||
runner = new CronRunner(store, automationStore, { onScheduleRunProcessed: onProcessed });
|
||||
await runner.executeSchedule(schedule);
|
||||
|
||||
expect(receivedSchedule).toEqual(schedule);
|
||||
expect(receivedResult).toBeDefined();
|
||||
expect(receivedResult!.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("concurrent schedule prevention in tick", () => {
|
||||
|
||||
@@ -34,6 +34,26 @@ export interface CronRunnerOptions {
|
||||
pollIntervalMs?: number;
|
||||
/** Optional AI prompt executor. When not provided, ai-prompt steps return a configuration error. */
|
||||
aiPromptExecutor?: AiPromptExecutor;
|
||||
/**
|
||||
* Optional post-run callback invoked after schedule execution and run recording.
|
||||
*
|
||||
* Called as a best-effort side effect — callback errors are logged but do not
|
||||
* alter the returned `AutomationRunResult` or flip successful runs to failed.
|
||||
* This keeps post-processing isolated from the core execution contract.
|
||||
*
|
||||
* The callback receives:
|
||||
* - `schedule`: The schedule that was executed
|
||||
* - `result`: The `AutomationRunResult` from execution (success/failure)
|
||||
*
|
||||
* Use this for schedule-specific processing such as:
|
||||
* - Persisting memory insight extraction results
|
||||
* - Updating external systems based on automation output
|
||||
* - Triggering downstream side effects
|
||||
*/
|
||||
onScheduleRunProcessed?: (
|
||||
schedule: ScheduledTask,
|
||||
result: AutomationRunResult,
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +70,7 @@ export class CronRunner {
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private pollIntervalMs: number;
|
||||
private aiPromptExecutor?: AiPromptExecutor;
|
||||
private onScheduleRunProcessed?: CronRunnerOptions["onScheduleRunProcessed"];
|
||||
/** Schedule IDs currently being executed — prevents concurrent runs of the same schedule. */
|
||||
private inFlight = new Set<string>();
|
||||
|
||||
@@ -63,6 +84,7 @@ export class CronRunner {
|
||||
options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
||||
);
|
||||
this.aiPromptExecutor = options.aiPromptExecutor;
|
||||
this.onScheduleRunProcessed = options.onScheduleRunProcessed;
|
||||
}
|
||||
|
||||
/** Start the polling loop. */
|
||||
@@ -163,6 +185,18 @@ export class CronRunner {
|
||||
log.error(`Failed to record run for ${schedule.id}: ${(recordErr as Error).message}`);
|
||||
}
|
||||
|
||||
// Invoke post-run callback (best-effort side effect)
|
||||
// Errors here do not alter the returned result or flip success/failure
|
||||
if (this.onScheduleRunProcessed) {
|
||||
try {
|
||||
await this.onScheduleRunProcessed(schedule, result);
|
||||
} catch (callbackErr) {
|
||||
log.error(
|
||||
`Post-run callback failed for ${schedule.name} (${schedule.id}): ${(callbackErr as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user