fix(FN-1952): recover failed review tasks
This commit is contained in:
52
packages/engine/src/auth-storage.test.ts
Normal file
52
packages/engine/src/auth-storage.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createFusionAuthStorage, getFusionAuthPath } from "./auth-storage.js";
|
||||
|
||||
describe("createFusionAuthStorage", () => {
|
||||
const originalHome = process.env.HOME;
|
||||
let homeDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
homeDir = await mkdtemp(join(tmpdir(), "fusion-engine-auth-"));
|
||||
process.env.HOME = homeDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
});
|
||||
|
||||
it("writes to Fusion auth and reads legacy Pi auth as fallback", async () => {
|
||||
const legacyAgentDir = join(homeDir, ".pi", "agent");
|
||||
mkdirSync(legacyAgentDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(legacyAgentDir, "auth.json"),
|
||||
JSON.stringify({
|
||||
openrouter: { type: "api_key", key: "legacy-openrouter-key" },
|
||||
minimax: { type: "api_key", key: "legacy-minimax-key" },
|
||||
}),
|
||||
);
|
||||
|
||||
const authStorage = createFusionAuthStorage();
|
||||
authStorage.set("openrouter", { type: "api_key", key: "fusion-openrouter-key" });
|
||||
|
||||
expect(await authStorage.getApiKey("openrouter")).toBe("fusion-openrouter-key");
|
||||
expect(await authStorage.getApiKey("minimax")).toBe("legacy-minimax-key");
|
||||
expect(authStorage.get("minimax")).toEqual({ type: "api_key", key: "legacy-minimax-key" });
|
||||
expect(existsSync(getFusionAuthPath(homeDir))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not create missing legacy Pi auth files", async () => {
|
||||
const authStorage = createFusionAuthStorage();
|
||||
|
||||
expect(await authStorage.getApiKey("openrouter")).toBeUndefined();
|
||||
expect(existsSync(join(homeDir, ".pi", "agent", "auth.json"))).toBe(false);
|
||||
expect(existsSync(join(homeDir, ".pi", "auth.json"))).toBe(false);
|
||||
});
|
||||
});
|
||||
94
packages/engine/src/auth-storage.ts
Normal file
94
packages/engine/src/auth-storage.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
type StoredCredential = { type?: string; key?: string };
|
||||
|
||||
function getHomeDir(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE || homedir();
|
||||
}
|
||||
|
||||
export function getFusionAuthPath(home = getHomeDir()): string {
|
||||
return join(home, ".fusion", "agent", "auth.json");
|
||||
}
|
||||
|
||||
function getLegacyAuthPaths(home = getHomeDir()): string[] {
|
||||
return [
|
||||
join(home, ".pi", "agent", "auth.json"),
|
||||
join(home, ".pi", "auth.json"),
|
||||
];
|
||||
}
|
||||
|
||||
function readLegacyCredentials(authPaths = getLegacyAuthPaths()): Record<string, StoredCredential> {
|
||||
const credentials: Record<string, StoredCredential> = {};
|
||||
|
||||
for (const authPath of authPaths) {
|
||||
if (!existsSync(authPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, StoredCredential>;
|
||||
for (const [provider, credential] of Object.entries(parsed)) {
|
||||
credentials[provider] ??= credential;
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid legacy auth files and continue with other candidates.
|
||||
}
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function resolveStoredApiKey(key: string | undefined): string | undefined {
|
||||
if (!key) return undefined;
|
||||
return process.env[key] ?? key;
|
||||
}
|
||||
|
||||
export function createFusionAuthStorage(): AuthStorage {
|
||||
const primary = AuthStorage.create(getFusionAuthPath());
|
||||
let legacyCredentials = readLegacyCredentials();
|
||||
|
||||
return new Proxy(primary, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "reload") {
|
||||
return () => {
|
||||
target.reload();
|
||||
legacyCredentials = readLegacyCredentials();
|
||||
};
|
||||
}
|
||||
|
||||
if (prop === "get") {
|
||||
return (provider: string) => target.get(provider) ?? legacyCredentials[provider];
|
||||
}
|
||||
|
||||
if (prop === "has") {
|
||||
return (provider: string) => target.has(provider) || provider in legacyCredentials;
|
||||
}
|
||||
|
||||
if (prop === "hasAuth") {
|
||||
return (provider: string) => target.hasAuth(provider) || Boolean(legacyCredentials[provider]);
|
||||
}
|
||||
|
||||
if (prop === "getAll") {
|
||||
return () => ({ ...legacyCredentials, ...target.getAll() });
|
||||
}
|
||||
|
||||
if (prop === "list") {
|
||||
return () => Array.from(new Set([...Object.keys(legacyCredentials), ...target.list()]));
|
||||
}
|
||||
|
||||
if (prop === "getApiKey") {
|
||||
return async (provider: string) => {
|
||||
const primaryKey = await target.getApiKey(provider);
|
||||
if (primaryKey) return primaryKey;
|
||||
|
||||
const credential = legacyCredentials[provider];
|
||||
return credential?.type === "api_key" ? resolveStoredApiKey(credential.key) : undefined;
|
||||
};
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
}) as AuthStorage;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { createKbAgent, describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
|
||||
import { AuthStorage, ModelRegistry, SessionManager, getAgentDir, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { ModelRegistry, SessionManager, getAgentDir, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
|
||||
import { isRegisteredGitWorktree, isUsableTaskWorktree, type WorktreePool } from "./worktree-pool.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
createTaskLogTool as sharedCreateTaskLogTool,
|
||||
} from "./agent-tools.js";
|
||||
import { getTaskCompletionBlockerForStore } from "./task-completion.js";
|
||||
import { createFusionAuthStorage } from "./auth-storage.js";
|
||||
|
||||
// Re-export for backward compatibility (tests import from executor.ts)
|
||||
export { summarizeToolArgs } from "./agent-logger.js";
|
||||
@@ -390,7 +391,7 @@ export class TaskExecutor {
|
||||
|
||||
private get modelRegistry(): InstanceType<typeof ModelRegistry> {
|
||||
if (!this._modelRegistry) {
|
||||
const authStorage = AuthStorage.create();
|
||||
const authStorage = createFusionAuthStorage();
|
||||
this._modelRegistry = new ModelRegistry(authStorage, join(getAgentDir(), "models.json"));
|
||||
this._modelRegistry.refresh();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Shared pi SDK setup for fn engine agents.
|
||||
*
|
||||
* Uses the user's existing pi auth (API keys / OAuth from ~/.pi/agent/auth.json).
|
||||
* Uses Fusion auth for writes and legacy pi auth as a read-only fallback.
|
||||
* Provides factory functions for creating triage and executor agent sessions.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,6 @@ import { join, relative, isAbsolute, resolve } from "node:path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import {
|
||||
AuthStorage,
|
||||
createAgentSession,
|
||||
createCodingTools,
|
||||
createExtensionRuntime,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
type SkillSelectionContext,
|
||||
} from "./skill-resolver.js";
|
||||
import { isContextLimitError } from "./context-limit-detector.js";
|
||||
import { createFusionAuthStorage } from "./auth-storage.js";
|
||||
|
||||
export interface AgentResult {
|
||||
session: AgentSession;
|
||||
@@ -474,7 +474,7 @@ export function wrapToolsWithBoundary(
|
||||
*/
|
||||
export async function createKbAgent(options: AgentOptions): Promise<AgentResult> {
|
||||
console.error(`[pi] createKbAgent called (cwd=${options.cwd}, tools=${options.tools}, provider=${options.defaultProvider}, model=${options.defaultModelId})`);
|
||||
const authStorage = AuthStorage.create();
|
||||
const authStorage = createFusionAuthStorage();
|
||||
const modelRegistry = new ModelRegistry(authStorage, join(getAgentDir(), "models.json"));
|
||||
await registerExtensionProviders(options.cwd, modelRegistry);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user