fix(FN-2545): persist memory audit extraction state and polish MemoryView
- Persist extract-to-audit continuity state in memory insights so extraction progress survives follow-up runs - Add core and dashboard route tests that cover audit extraction flow and prevent regressions - Restore lint/typecheck baseline by removing obsolete planning subtask route wiring from dashboard routes - Apply MemoryView UX refinements with updated component logic and dedicated styling adjustments
This commit is contained in:
@@ -14118,6 +14118,31 @@ describe("GET /api/memory/audit", () => {
|
||||
expect(res.body.workingMemory).toHaveProperty("size");
|
||||
expect(res.body.workingMemory).toHaveProperty("sectionCount");
|
||||
});
|
||||
|
||||
it("preserves extraction metadata across extract then audit requests", async () => {
|
||||
writeFileSync(
|
||||
join(rootDir, ".fusion", "memory", "MEMORY.md"),
|
||||
"## Architecture\n\nDurable architecture\n\n## Conventions\n\nDurable conventions\n\n## Pitfalls\n\nDurable pitfalls",
|
||||
);
|
||||
|
||||
const extractRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/memory/extract",
|
||||
JSON.stringify({}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(extractRes.status).toBe(200);
|
||||
expect(extractRes.body.success).toBe(true);
|
||||
|
||||
const auditRes = await GET(buildApp(), "/api/memory/audit");
|
||||
|
||||
expect(auditRes.status).toBe(200);
|
||||
expect(auditRes.body.extraction.runAt).toBeTruthy();
|
||||
expect(auditRes.body.extraction.summary).not.toBe("No extraction runs recorded");
|
||||
expect(auditRes.body.checks.find((check: { id: string; details: string }) => check.id === "recent-extraction")?.details).not.toContain("No extraction runs recorded");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/memory/stats", () => {
|
||||
|
||||
@@ -2147,61 +2147,6 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
const heartbeatMonitor = options?.heartbeatMonitor;
|
||||
const hasHeartbeatExecutor = Boolean(heartbeatMonitor);
|
||||
|
||||
const REMOTE_MIN_TTL_MS = 60_000;
|
||||
const REMOTE_MAX_TTL_MS = 86_400_000;
|
||||
const remoteShortLivedTokens = new Map<string, { expiresAt: number }>();
|
||||
|
||||
function generateRemoteToken(): string {
|
||||
return `rtok_${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function maskRemoteToken(token: string): string {
|
||||
if (token.length <= 8) return "********";
|
||||
return `${token.slice(0, 4)}…${token.slice(-4)}`;
|
||||
}
|
||||
|
||||
async function ensurePersistentRemoteToken(scopedStore: TaskStore): Promise<string> {
|
||||
const settings = await scopedStore.getSettings();
|
||||
const existing = typeof settings.remotePersistentToken === "string" ? settings.remotePersistentToken : "";
|
||||
if (existing) return existing;
|
||||
const token = generateRemoteToken();
|
||||
await scopedStore.updateSettings({ remotePersistentToken: token });
|
||||
return token;
|
||||
}
|
||||
|
||||
function resolveRemoteOrigin(req: Request): string {
|
||||
const protocol = req.protocol || "http";
|
||||
const hostHeader = req.get("host") ?? "127.0.0.1:4040";
|
||||
return `${protocol}://${hostHeader}`;
|
||||
}
|
||||
|
||||
async function buildRemoteUrlForTokenType(
|
||||
scopedStore: TaskStore,
|
||||
req: Request,
|
||||
tokenType: "persistent" | "short-lived",
|
||||
ttlMs?: number,
|
||||
): Promise<{ url: string; tokenType: "persistent" | "short-lived"; expiresAt: string | null }> {
|
||||
const baseUrl = new URL(resolveRemoteOrigin(req));
|
||||
let token: string;
|
||||
let expiresAt: string | null = null;
|
||||
|
||||
if (tokenType === "short-lived") {
|
||||
const ttl = Math.floor(Number(ttlMs ?? 900_000));
|
||||
if (!Number.isFinite(ttl) || ttl < REMOTE_MIN_TTL_MS || ttl > REMOTE_MAX_TTL_MS) {
|
||||
throw new ApiError(400, "Short-lived token ttlMs out of range", { code: "INVALID_TTL" });
|
||||
}
|
||||
token = generateRemoteToken();
|
||||
const expiryMs = Date.now() + ttl;
|
||||
remoteShortLivedTokens.set(token, { expiresAt: expiryMs });
|
||||
expiresAt = new Date(expiryMs).toISOString();
|
||||
} else {
|
||||
token = await ensurePersistentRemoteToken(scopedStore);
|
||||
}
|
||||
|
||||
baseUrl.searchParams.set("token", token);
|
||||
return { url: baseUrl.toString(), tokenType, expiresAt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the heartbeatMonitor is bound to the same project as scopedStore.
|
||||
* Returns false when the monitor's rootDir is set and differs from the store's root.
|
||||
|
||||
@@ -14,7 +14,7 @@ interface PlanningSubtaskRouteDeps {
|
||||
|
||||
export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: PlanningSubtaskRouteDeps): void {
|
||||
const { router, getProjectContext, planningLogger, rethrowAsApiError } = ctx;
|
||||
const { store, aiSessionStore, checkSessionLock, parseLastEventId, replayBufferedSSE } = deps;
|
||||
const { aiSessionStore, checkSessionLock, parseLastEventId, replayBufferedSSE } = deps;
|
||||
|
||||
// ── Planning Mode Routes ──────────────────────────────────────────────────
|
||||
// UTILITY PATH: Planning and subtask session routes are on a separate control-plane lane.
|
||||
@@ -185,7 +185,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
}
|
||||
|
||||
// Fetch parent task to inherit model settings if parentTaskId is provided
|
||||
let parentTask: Awaited<ReturnType<typeof store.getTask>> | undefined;
|
||||
let parentTask: Awaited<ReturnType<TaskStore["getTask"]>> | undefined;
|
||||
if (typeof parentTaskId === "string" && parentTaskId.trim()) {
|
||||
try {
|
||||
parentTask = await scopedStore.getTask(parentTaskId);
|
||||
@@ -195,7 +195,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
}
|
||||
}
|
||||
|
||||
const createdTasks = [] as Awaited<ReturnType<typeof store.createTask>>[];
|
||||
const createdTasks = [] as Awaited<ReturnType<TaskStore["createTask"]>>[];
|
||||
const tempIdToTaskId = new Map<string, string>();
|
||||
|
||||
for (const item of subtasks) {
|
||||
@@ -912,7 +912,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
}
|
||||
}
|
||||
|
||||
const createdTasks = [] as Awaited<ReturnType<typeof store.createTask>>[];
|
||||
const createdTasks = [] as Awaited<ReturnType<TaskStore["createTask"]>>[];
|
||||
const tempIdToTaskId = new Map<string, string>();
|
||||
|
||||
// Create tasks
|
||||
|
||||
Reference in New Issue
Block a user