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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user