fix(FN-1952): restore pi auth and extension loading

This commit is contained in:
gsxdsm
2026-04-16 21:42:16 -07:00
parent a99af24297
commit 8df18fab2b
30 changed files with 644 additions and 72 deletions

View File

@@ -57,8 +57,11 @@ async function initEngine() {
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Constants ───────────────────────────────────────────────────────────────
@@ -212,6 +215,7 @@ function cleanupExpiredSessions(): void {
}
const cleanupInterval = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
process.on("beforeExit", () => {
clearInterval(cleanupInterval);
@@ -461,7 +465,7 @@ export async function generateAgentSpec(
}
try {
await engineReady;
await ensureEngineReady();
await promptCatalogReadyPromise;
const spec = await generateSpecWithAI(session, rootDir, promptOverrides);
session.spec = spec;

View File

@@ -33,8 +33,11 @@ async function initEngine() {
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Types ───────────────────────────────────────────────────────────────────
@@ -194,6 +197,7 @@ function cleanupExpiredRateLimits(): void {
// Start cleanup interval
const cleanupInterval = setInterval(cleanupExpiredRateLimits, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
// Handle graceful shutdown
process.on("beforeExit", () => {
@@ -264,7 +268,7 @@ export async function refineText(
promptOverrides?: PromptOverrideMap,
): Promise<string> {
// Ensure engine is loaded before using createKbAgent
await engineReady;
await ensureEngineReady();
if (!createKbAgent) {
throw new AiServiceError("AI engine not available");

View File

@@ -47,8 +47,11 @@ async function initEngine() {
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Constants ───────────────────────────────────────────────────────────────
@@ -348,7 +351,7 @@ export class ChatManager {
try {
// Ensure engine is loaded
await engineReady;
await ensureEngineReady();
if (!createKbAgent) {
throw new Error("AI agent not available");

View File

@@ -118,6 +118,7 @@ export class GitHubPollingService extends EventEmitter<GitHubPollingServiceEvent
this.timer = setInterval(() => {
void this.pollOnce();
}, this.pollingIntervalMs);
this.timer.unref?.();
void this.pollOnce();
}
@@ -409,4 +410,3 @@ function hasIssueBadgeChanged(current: IssueInfo | undefined, next: IssueInfo):
current.title !== next.title ||
current.stateReason !== next.stateReason;
}

View File

@@ -101,7 +101,11 @@ async function initEngine() {
}
}
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Constants ───────────────────────────────────────────────────────────────
@@ -492,6 +496,7 @@ function cleanupExpiredSessions(): void {
}
const cleanupInterval = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
process.on("beforeExit", () => clearInterval(cleanupInterval));
// ── Stream Manager ──────────────────────────────────────────────────────────
@@ -671,7 +676,7 @@ async function createTargetInterviewAgent(
session: TargetInterviewSession,
rootDir: string,
): Promise<AgentResult> {
await engineReady;
await ensureEngineReady();
return createKbAgent({
cwd: rootDir,

View File

@@ -41,7 +41,11 @@ async function initEngine() {
}
}
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Constants ───────────────────────────────────────────────────────────────
@@ -392,6 +396,7 @@ function cleanupExpiredSessions(): void {
}
const cleanupInterval = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
process.on("beforeExit", () => clearInterval(cleanupInterval));
// ── Stream Manager ──────────────────────────────────────────────────────────
@@ -748,7 +753,7 @@ async function createMissionInterviewAgent(
rootDir: string,
promptOverrides?: PromptOverrideMap,
): Promise<AgentResult> {
await engineReady;
await ensureEngineReady();
const effectivePrompt = resolvePrompt("mission-interview-system", promptOverrides);

View File

@@ -50,8 +50,11 @@ async function initEngine() {
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
// ── Constants ───────────────────────────────────────────────────────────────
@@ -360,6 +363,7 @@ function cleanupExpiredSessions(): void {
// Start cleanup interval
const cleanupInterval = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
// Handle graceful shutdown
process.on("beforeExit", () => {
@@ -576,7 +580,7 @@ export async function createSession(
// Create AI agent and get the first question
// Only await engineReady if createKbAgent hasn't been set externally (e.g., via __setCreateKbAgent)
if (!createKbAgent) {
await engineReady;
await ensureEngineReady();
}
const agentResult = await createKbAgent({
@@ -802,7 +806,7 @@ async function createPlanningAgent(
promptOverrides?: PromptOverrideMap,
): Promise<AgentResult> {
// Ensure engine is loaded before using createKbAgent
await engineReady;
await ensureEngineReady();
// Resolve the effective system prompt (override or default)
const systemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;

View File

@@ -18,7 +18,7 @@ import * as nodeFs from "node:fs";
import { promisify } from "node:util";
import type { TaskStore, Column, ScheduleType, ActivityEventType, ModelPreset, MessageType, ParticipantType, RoutineTriggerType, ProjectSettings } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupAutomation, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, readMemory, writeMemory, MemoryBackendError } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, getCurrentRepo, isGhAuthenticated, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupAutomation, exportSettings, importSettings, validateImportData, MessageStore, RoutineStore, isWebhookTrigger, resolveMemoryBackend, getMemoryBackendCapabilities, listMemoryBackendTypes, readMemory, writeMemory, MemoryBackendError, discoverPiExtensions, updatePiExtensionDisabledIds } from "@fusion/core";
import type { ServerOptions } from "./server.js";
import { GitHubClient, parseBadgeUrl } from "./github.js";
import { githubRateLimiter } from "./github-poll.js";
@@ -2838,6 +2838,43 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
/**
* GET /api/settings/pi-extensions
* List Pi/Fusion extension entry points and their Fusion-owned enabled state.
*/
router.get("/settings/pi-extensions", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
res.json(discoverPiExtensions(scopedStore.getRootDir()));
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;
}
rethrowAsApiError(err);
}
});
/**
* PUT /api/settings/pi-extensions
* Persist Fusion-owned disabled extension ids in ~/.fusion/agent/settings.json.
*/
router.put("/settings/pi-extensions", async (req, res) => {
try {
const disabledIds = (req.body as { disabledIds?: unknown }).disabledIds;
if (!Array.isArray(disabledIds) || disabledIds.some((entry) => typeof entry !== "string")) {
throw badRequest("disabledIds must be an array of extension ids");
}
const { store: scopedStore } = await getProjectContext(req);
res.json(updatePiExtensionDisabledIds(scopedStore.getRootDir(), disabledIds));
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;
}
rethrowAsApiError(err);
}
});
/**
* POST /api/settings/test-ntfy
* Send a test notification to verify ntfy configuration.
@@ -5655,6 +5692,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
}
}, batchImportWindowMs);
batchImportCleanupInterval.unref?.();
}
return (req: Request, res: Response, next: NextFunction): void => {

View File

@@ -20,7 +20,11 @@ async function initEngine() {
}
}
const engineReady = initEngine();
let engineReady: Promise<void> | undefined;
function ensureEngineReady() {
engineReady ??= initEngine();
return engineReady;
}
export interface SubtaskItem {
id: string;
@@ -263,6 +267,7 @@ function cleanupExpiredSessions(): void {
}
const cleanupInterval = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
cleanupInterval.unref?.();
process.on("beforeExit", () => {
clearInterval(cleanupInterval);
});
@@ -396,7 +401,7 @@ async function generateSubtasks(
const session = sessions.get(sessionId);
if (!session) throw new SessionNotFoundError(`Subtask session ${sessionId} not found`);
await engineReady;
await ensureEngineReady();
// Resolve the effective system prompt (override or default)
const systemPrompt = resolvePrompt("subtask-breakdown-system", promptOverrides) || SUBTASK_BREAKDOWN_PROMPT;

View File

@@ -335,6 +335,7 @@ export class WebSocketManager extends EventEmitter<WebSocketManagerEvents> {
}
}
}, this.heartbeatIntervalMs);
this.heartbeatTimer.unref?.();
}
private clearHeartbeat(): void {