Files
fusion/plugins/fusion-plugin-compound-engineering/src/session/session-recovery.ts
gsxdsm d8f0b1a268 Restore PostgreSQL integration parity (#2089)
## Summary

- add asynchronous PostgreSQL parity to research commands and engine
execution paths
- persist Roadmap, Compound Engineering sessions, and WhatsApp state in
PostgreSQL
- harden cancellation, concurrency, reconnect, replay-claim, and
detached-promise behavior
- bundle the PostgreSQL-backed integration implementations in the
published CLI

This is PR 2 of 2 and is intentionally stacked on #2088. It contains 44
changed files; merge #2088 first, then retarget this PR to `main` if
GitHub does not do so automatically.

## Verification

- `pnpm check:changesets --strict`
- `pnpm lint`
- `pnpm test:gate`: 463 tests passed
- Compound Engineering plugin: 299 tests passed
- Roadmap plugin: 144 tests passed
- WhatsApp plugin: 27 tests passed
- research CLI: 18 tests passed
- `pnpm verify:fast`: all scoped typechecks, builds, CLI build, and boot
smoke passed

## Post-Deploy Monitoring & Validation

- deploy only after #2088 and verify schema migration `0002` is present
- monitor research cancellation, automation claims, agent execution,
plugin schema initialization, and unhandled rejections
- validate Roadmap ownership, Compound Engineering session recovery, and
WhatsApp reconnect/replay deduplication
- compare per-project plugin and workflow counts after cutover
- restore the pre-deploy backup for data rollback; avoid an in-place
schema downgrade
2026-07-14 08:17:36 -07:00

50 lines
1.6 KiB
TypeScript

import type { PluginContext } from "@fusion/core";
import { getCeSessionStore } from "./session-store.js";
const DEFAULT_RECOVERY_SCAN_TTL_MS = 120_000;
const lastRecoveryScanAt = new WeakMap<object, number>();
interface RecoverStaleSessionsOptions {
reason: "load" | "route";
force?: boolean;
emitEvent?: boolean;
now?: number;
ttlMs?: number;
}
/**
* Best-effort stale-session recovery for persisted CE sessions that outlived
* their in-memory agent handle. Route callers use a TTL because the individual
* session endpoint is also the dashboard polling fallback.
*/
export async function recoverStaleSessionsForContext(
ctx: PluginContext,
options: RecoverStaleSessionsOptions,
): Promise<string[]> {
const key = ctx.taskStore as object;
const now = options.now ?? Date.now();
const ttlMs = options.ttlMs ?? DEFAULT_RECOVERY_SCAN_TTL_MS;
if (!options.force) {
const last = lastRecoveryScanAt.get(key) ?? 0;
if (now - last < ttlMs) return [];
}
lastRecoveryScanAt.set(key, now);
try {
const recovered = await getCeSessionStore(ctx).recoverStaleSessionsAsync(now);
if (recovered.length > 0) {
ctx.logger.info(`Compound Engineering recovered stale session(s) during ${options.reason}: ${recovered.join(", ")}`);
if (options.emitEvent) {
ctx.emitEvent("compound-engineering:sessions-recovered", { sessionIds: recovered, reason: options.reason });
}
}
return recovered;
} catch (err) {
ctx.logger.warn(
`Compound Engineering stale-session recovery skipped during ${options.reason}: ${err instanceof Error ? err.message : String(err)}`,
);
return [];
}
}