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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }> {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user