fix(FN-XXXX): quiet pi-claude-cli MCP config refresh log

Demote the refresh message from console.error to debugMcp so it no
longer appears as an error in normal output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-01 22:58:00 -07:00
parent 24b3ded03f
commit 31195374d4
13 changed files with 852 additions and 73 deletions

View File

@@ -15,6 +15,8 @@ const {
mockRecoverNoProgressNoTaskDoneFailures,
mockRunStartupRecovery,
mockExecutorCtor,
mockResumeOrphaned,
mockTaskStoreSettings,
mockMessageStoreSetHook,
} = vi.hoisted(() => ({
mockSelfHealingStart: vi.fn(),
@@ -23,6 +25,8 @@ const {
mockRecoverNoProgressNoTaskDoneFailures: vi.fn().mockResolvedValue(0),
mockRunStartupRecovery: vi.fn().mockResolvedValue(undefined),
mockExecutorCtor: vi.fn(),
mockResumeOrphaned: vi.fn().mockResolvedValue(undefined),
mockTaskStoreSettings: {} as Record<string, unknown>,
mockMessageStoreSetHook: vi.fn(),
}));
@@ -46,7 +50,7 @@ vi.mock("@fusion/core", async () => {
self.getDatabase = vi.fn().mockReturnValue(mockDatabase);
self.init = vi.fn().mockResolvedValue(undefined);
self.listTasks = vi.fn().mockResolvedValue([]);
self.getSettings = vi.fn().mockResolvedValue({});
self.getSettings = vi.fn().mockImplementation(async () => structuredClone(mockTaskStoreSettings));
self.getMissionStore = vi.fn().mockReturnValue({
getMissionWithHierarchy: vi.fn().mockReturnValue(null),
findNextPendingSlice: vi.fn().mockReturnValue(null),
@@ -141,7 +145,7 @@ vi.mock("../../executor.js", async () => {
TaskExecutor: vi.fn().mockImplementation((_store, _rootDir, options) => {
mockExecutorCtor(options);
const self = {} as Record<string, unknown>;
self.resumeOrphaned = vi.fn().mockResolvedValue(undefined);
self.resumeOrphaned = mockResumeOrphaned;
self.recoverCompletedTask = vi.fn().mockResolvedValue(true);
self.getExecutingTaskIds = vi.fn().mockReturnValue(new Set());
self.handleLoopDetected = vi.fn().mockResolvedValue(false);
@@ -184,6 +188,9 @@ describe("InProcessRuntime", () => {
}
beforeEach(() => {
for (const key of Object.keys(mockTaskStoreSettings)) {
delete mockTaskStoreSettings[key];
}
// Create a unique temp directory for this test run
testDir = mkdtempSync(join(tmpdir(), `fn-test-${randomUUID().slice(0, 8)}-`));
@@ -243,6 +250,55 @@ describe("InProcessRuntime", () => {
await runtime.start();
expect(mockRecoverNoProgressNoTaskDoneFailures).toHaveBeenCalledTimes(1);
expect(mockResumeOrphaned).toHaveBeenCalledTimes(1);
expect(mockRunStartupRecovery).toHaveBeenCalledTimes(1);
}, 30000);
it("defers startup recovery while enginePaused is active", async () => {
mockTaskStoreSettings.enginePaused = true;
await runtime.start();
expect(mockRecoverNoProgressNoTaskDoneFailures).not.toHaveBeenCalled();
expect(mockResumeOrphaned).not.toHaveBeenCalled();
expect(mockRunStartupRecovery).not.toHaveBeenCalled();
}, 30000);
it("resumes deferred startup recovery after engine pause is cleared in startup order", async () => {
mockTaskStoreSettings.enginePaused = true;
await runtime.start();
mockRecoverNoProgressNoTaskDoneFailures.mockClear();
mockResumeOrphaned.mockClear();
mockRunStartupRecovery.mockClear();
mockTaskStoreSettings.enginePaused = false;
await runtime.resumeAfterUnpause();
expect(mockRecoverNoProgressNoTaskDoneFailures).toHaveBeenCalledTimes(1);
expect(mockResumeOrphaned).toHaveBeenCalledTimes(1);
expect(mockRunStartupRecovery).toHaveBeenCalledTimes(1);
expect(mockRecoverNoProgressNoTaskDoneFailures.mock.invocationCallOrder[0]).toBeLessThan(
mockResumeOrphaned.mock.invocationCallOrder[0],
);
expect(mockResumeOrphaned.mock.invocationCallOrder[0]).toBeLessThan(
mockRunStartupRecovery.mock.invocationCallOrder[0],
);
}, 30000);
it("coalesces concurrent unpause recovery dispatches", async () => {
mockTaskStoreSettings.enginePaused = true;
await runtime.start();
mockRecoverNoProgressNoTaskDoneFailures.mockClear();
mockResumeOrphaned.mockClear();
mockRunStartupRecovery.mockClear();
mockTaskStoreSettings.enginePaused = false;
await Promise.all([runtime.resumeAfterUnpause(), runtime.resumeAfterUnpause()]);
expect(mockRecoverNoProgressNoTaskDoneFailures).toHaveBeenCalledTimes(1);
expect(mockResumeOrphaned).toHaveBeenCalledTimes(1);
expect(mockRunStartupRecovery).toHaveBeenCalledTimes(1);
}, 30000);

View File

@@ -116,6 +116,10 @@ export class InProcessRuntime
* before `start()` via `setMergeEnqueuer`.
*/
private mergeEnqueuer?: (taskId: string) => void;
/** Tracks whether startup recovery was intentionally deferred due to pause state. */
private startupRecoveryDeferred = false;
/** Prevent duplicate unpause recovery dispatches from racing each other. */
private resumeAfterUnpauseRunning = false;
/**
* @param config - Runtime configuration
@@ -669,20 +673,19 @@ export class InProcessRuntime
// 8. Set up event forwarding from TaskStore
this.setupEventForwarding();
// 9. Requeue no-progress no-task_done failures before resumeOrphaned
// can restart them.
await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
const startupSettings = await this.taskStore.getSettings();
if (startupSettings.globalPause || startupSettings.enginePaused) {
this.startupRecoveryDeferred = true;
runtimeLog.log(
`Startup recovery deferred — ${
startupSettings.globalPause ? "global pause" : "engine pause"
} is active`,
);
} else {
this.startupRecoveryDeferred = false;
// 10. Resume orphaned in-progress tasks
await this.executor.resumeOrphaned();
// Some "stuck" tasks are already orphaned by the time the runtime boots:
// they no longer have a tracked session/worktree, so the stuck detector
// cannot recover them. Delegate the startup recovery pass to
// SelfHealingManager so the policy lives in one place.
void this.selfHealingManager.runStartupRecovery().catch((err) => {
runtimeLog.error("Self-healing startup recovery failed:", err);
});
await this.resumeStartupRecoverySequence();
}
// 11. Start scheduler and triage processor
this.scheduler.start();
@@ -907,6 +910,62 @@ export class InProcessRuntime
this.mergeEnqueuer = enqueueMerge;
}
/**
* Resume executor/self-healing activity after an unpause transition.
*
* When startup recovery had been deferred, this replays the original startup
* ordering so orphan resume and self-healing cannot race each other.
*/
async resumeAfterUnpause(): Promise<void> {
if (!this.taskStore || !this.executor || !this.selfHealingManager) {
return;
}
if (this.resumeAfterUnpauseRunning) {
return;
}
this.resumeAfterUnpauseRunning = true;
try {
const settings = await this.taskStore.getSettings();
if (settings.globalPause || settings.enginePaused) {
runtimeLog.log(
`Unpause recovery still blocked — ${
settings.globalPause ? "global pause" : "engine pause"
} remains active`,
);
return;
}
if (this.startupRecoveryDeferred) {
await this.resumeStartupRecoverySequence();
this.startupRecoveryDeferred = false;
return;
}
await this.executor.resumeOrphaned();
} finally {
this.resumeAfterUnpauseRunning = false;
}
}
private async resumeStartupRecoverySequence(): Promise<void> {
// Requeue no-progress no-task_done failures before resumeOrphaned can
// restart other orphaned executions.
await this.selfHealingManager!.recoverNoProgressNoTaskDoneFailures();
// Resume orphaned in-progress tasks before the broader self-healing scan
// so the executor can claim or fast-path eligible tasks first.
await this.executor!.resumeOrphaned();
// Some "stuck" tasks are already orphaned by the time the runtime boots:
// they no longer have a tracked session/worktree, so the stuck detector
// cannot recover them. Delegate the startup recovery pass to
// SelfHealingManager so the policy lives in one place.
void this.selfHealingManager!.runStartupRecovery().catch((err) => {
runtimeLog.error("Self-healing startup recovery failed:", err);
});
}
/**
* Get the project's TaskStore instance.
* @throws Error if runtime has not been started