feat(FN-3451): add shared state snapshots for mesh sync

This merge completes the mesh sync shared-state pipeline (FN-3451 Steps 1–3) by adding agent run snapshot limits, applying inbound snapshots, and returning shared state snapshots, while hardening agent store cache initialization during sync. It also unifies agent action gating classifications across

Fusion-Task-Id: FN-3451
This commit is contained in:
Fusion
2026-05-08 14:03:17 -07:00
committed by gsxdsm
parent 07cd3d065c
commit ae6fdf9329
5 changed files with 172 additions and 6 deletions

View File

@@ -13,6 +13,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { AgentStore } from "../agent-store.js";
import { TaskStore } from "../store.js";
import { validateSnapshotEnvelope } from "../shared-mesh-state.js";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { mkdtempSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -2885,8 +2886,17 @@ describe("AgentStore", () => {
const agent = await store.createAgent({ name: "Snapshot Agent", role: "executor" });
await store.setLastBlockedState(agent.id, { taskId: "FN-1", blockedBy: "dep", recordedAt: new Date().toISOString(), contextHash: "h" });
const run1 = await store.startHeartbeatRun(agent.id);
await store.endHeartbeatRun(run1.id, "completed");
const run2 = await store.startHeartbeatRun(agent.id);
await store.endHeartbeatRun(run2.id, "completed");
const agentSnapshot = await store.getAgentSnapshot();
const runSnapshot = store.getAgentRunSnapshot();
const limitedRunSnapshot = store.getAgentRunSnapshot(1);
validateSnapshotEnvelope(agentSnapshot);
validateSnapshotEnvelope(runSnapshot);
const applyAgent = await store.applyAgentSnapshot(agentSnapshot);
const applyRun = await store.applyAgentRunSnapshot(runSnapshot);
@@ -2894,8 +2904,12 @@ describe("AgentStore", () => {
const runSnapshot2 = store.getAgentRunSnapshot();
expect(applyAgent.appliedAgents).toBeGreaterThan(0);
expect(agentSnapshot.payload.agents.length).toBeGreaterThan(0);
expect(agentSnapshot.payload.blockedStates.length).toBe(1);
expect(agentSnapshot2.payload).toEqual(agentSnapshot.payload);
expect(runSnapshot2.payload).toEqual(runSnapshot.payload);
expect(limitedRunSnapshot.payload.runs).toHaveLength(1);
expect(limitedRunSnapshot.payload.runs[0]?.id).toBe(run2.id);
expect(applyRun.applied + applyRun.skipped).toBeGreaterThanOrEqual(0);
});
});

View File

@@ -2189,12 +2189,19 @@ export class AgentStore extends EventEmitter {
return { appliedAgents, appliedBlockedStates };
}
getAgentRunSnapshot(): AgentRunSnapshot {
const runs = this.db.prepare("SELECT data FROM agentRuns ORDER BY startedAt ASC").all() as Array<{ data: string }>;
const parsed = runs
getAgentRunSnapshot(limit?: number): AgentRunSnapshot {
const normalizedLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : undefined;
const query = normalizedLimit
? "SELECT data FROM agentRuns ORDER BY startedAt DESC LIMIT ?"
: "SELECT data FROM agentRuns ORDER BY startedAt ASC";
const rows = normalizedLimit
? (this.db.prepare(query).all(normalizedLimit) as Array<{ data: string }>)
: (this.db.prepare(query).all() as Array<{ data: string }>);
const parsed = rows
.map((row) => this.parseJson<AgentHeartbeatRun | null>(row.data, null))
.filter((run): run is AgentHeartbeatRun => run !== null);
return createAgentRunSnapshot(parsed);
const orderedRuns = normalizedLimit ? parsed.reverse() : parsed;
return createAgentRunSnapshot(orderedRuns);
}
async applyAgentRunSnapshot(snapshot: AgentRunSnapshot): Promise<{ applied: number; skipped: number }> {

View File

@@ -3120,11 +3120,11 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
async applyProjectSettingsSnapshot(snapshot: ProjectSettingsSnapshot): Promise<SettingsSyncResult> {
validateSnapshotEnvelope(snapshot);
const payloadWithoutChecksum: Omit<SettingsSyncPayload, "checksum"> = {
version: 1,
exportedAt: snapshot.exportedAt,
global: snapshot.payload.global,
projects: snapshot.payload.projects,
providerAuth: undefined,
exportedAt: snapshot.exportedAt,
version: 1,
};
const checksum = createHash("sha256")
.update(JSON.stringify(payloadWithoutChecksum))

View File

@@ -22,6 +22,8 @@ import type {
Slice,
} from "./mission-types.js";
export const SHARED_STATE_DEFAULT_LIMIT = 10_000;
export interface SharedSnapshotEnvelope<TPayload> {
version: number;
exportedAt: string;