feat(FN-2245): enforce shared diagnostics in planning interview flows

- Migrate mission, milestone-slice, and subtask breakdown flows to shared ai-session-diagnostics logging
- Add a planning-flow guardrail test that fails on raw console.* diagnostics across planning modules
- Update mission and subtask breakdown tests to use structured diagnostics sink hooks instead of console spies
- Keep backward-compatible diagnostics test hooks and reset injected sinks to avoid cross-test leakage
This commit is contained in:
Fusion
2026-04-22 09:39:34 -07:00
committed by gsxdsm
parent cf0d52b0c2
commit 68ebf50f4d
6 changed files with 424 additions and 225 deletions

View File

@@ -24,6 +24,11 @@ import {
extractJsonCandidate,
repairJson,
} from "./mission-interview.js";
import {
createSessionDiagnostics,
resetDiagnosticsSink,
nonfatal,
} from "./ai-session-diagnostics.js";
// Re-export JSON parsing utilities from mission-interview for external consumers
export {
@@ -32,6 +37,13 @@ export {
repairJson,
} from "./mission-interview.js";
/**
* Shared diagnostics helper for the milestone-slice-interview module.
* Uses the shared ai-session-diagnostics helper for consistent scoped logging.
* @see ai-session-diagnostics.ts for the shared contract
*/
const diagnostics = createSessionDiagnostics("milestone-slice-interview");
/**
* Parse a target interview response (milestone or slice) from the AI agent.
* Validates the response structure and extracts the typed data.
@@ -40,7 +52,7 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
const candidate = extractJsonCandidate(text);
if (!candidate) {
console.error("[milestone-slice-interview] No JSON candidate found in agent response:", text.slice(0, 500));
diagnostics.error("No JSON candidate found in agent response", { inputSnippet: text.slice(0, 500), operation: "parse-json" });
throw new Error("AI returned no valid JSON. Please try again.");
}
@@ -52,7 +64,7 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
const repaired = repairJson(candidate);
parsed = JSON.parse(repaired);
} catch (repairErr) {
console.error("[milestone-slice-interview] Failed to parse agent response:", candidate.slice(0, 500));
diagnostics.error("Failed to parse agent response (repair also failed)", { inputSnippet: candidate.slice(0, 500), operation: "parse-json-repair" });
throw new Error(
`Failed to parse AI response: ${repairErr instanceof Error ? repairErr.message : "Unknown error"}. Please try again.`
);
@@ -75,7 +87,7 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
}
}
console.error("[milestone-slice-interview] Invalid response structure:", JSON.stringify(parsed).slice(0, 500));
diagnostics.error("Invalid response structure from AI", { parsedSnippet: JSON.stringify(parsed).slice(0, 500), operation: "parse-validate" });
throw new Error("AI returned an invalid response structure. Please try again.");
}
@@ -461,7 +473,7 @@ export function rehydrateFromStore(store: AiSessionStore): number {
(row) => row.type === "milestone_interview" || row.type === "slice_interview"
);
} catch (error) {
console.error("[milestone-slice-interview] Failed to list recoverable sessions:", error);
diagnostics.errorFromException("Failed to list recoverable sessions", error, { operation: "list-recoverable" });
return 0;
}
@@ -472,7 +484,7 @@ export function rehydrateFromStore(store: AiSessionStore): number {
sessions.set(session.id, session);
rehydrated += 1;
} catch (error) {
console.error(`[milestone-slice-interview] Failed to rehydrate session ${row.id}:`, error);
diagnostics.errorFromException("Failed to rehydrate session", error, { sessionId: row.id, operation: "rehydrate" });
}
}
@@ -541,11 +553,12 @@ export class MilestoneSliceInterviewStreamManager extends EventEmitter {
if (!callbacks) return eventId;
for (const callback of callbacks) {
try {
callback(event, eventId);
} catch (err) {
console.error(`[milestone-slice-interview] Error broadcasting to client for session ${sessionId}:`, err);
}
nonfatal(
() => callback(event, eventId),
diagnostics,
"Error broadcasting to client",
{ sessionId, operation: "broadcast" }
);
}
return eventId;
@@ -657,11 +670,12 @@ function disposeAgentForRetry(session: TargetInterviewSession): void {
return;
}
try {
session.agent.session.dispose?.();
} catch (error) {
console.error(`[milestone-slice-interview] Error disposing agent for retry in session ${session.id}:`, error);
}
nonfatal(
() => session.agent.session.dispose?.(),
diagnostics,
"Error disposing agent for retry",
{ sessionId: session.id, operation: "dispose-retry" }
);
session.agent = undefined;
}
@@ -770,7 +784,7 @@ async function initializeAgent(session: TargetInterviewSession, rootDir: string)
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to initialize AI agent";
console.error(`[milestone-slice-interview] Agent initialization error for session ${session.id}:`, err);
diagnostics.errorFromException("Agent initialization error for session", err, { sessionId: session.id, operation: "initialize-agent" });
session.error = errorMessage;
session.updatedAt = new Date();
persistSession(session, "error", errorMessage);
@@ -828,8 +842,9 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
lastError = err instanceof Error ? err : new Error(String(err));
if (attempt < MAX_PARSE_RETRIES) {
console.warn(
`[milestone-slice-interview] Parse attempt ${attempt + 1} failed for session ${session.id}, requesting reformat`
diagnostics.warn(
"Parse attempt failed, requesting reformat",
{ sessionId: session.id, attempt: attempt + 1, operation: "parse-retry" }
);
try {
session.thinkingOutput = "";
@@ -857,7 +872,7 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
}
responseText = retryText;
} catch (retryErr) {
console.error(`[milestone-slice-interview] Retry prompt failed for session ${session.id}:`, retryErr);
diagnostics.errorFromException("Retry prompt failed for session", retryErr, { sessionId: session.id, operation: "retry-prompt" });
break;
}
}
@@ -866,7 +881,10 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
if (!parsed) {
const errorMsg = `${lastError?.message || "Failed to parse AI response"} You can try responding again or start a new session.`;
console.error(`[milestone-slice-interview] All parse attempts exhausted for session ${session.id}:`, errorMsg);
diagnostics.error(
"All parse attempts exhausted for session",
{ sessionId: session.id, message: errorMsg, operation: "parse-exhausted" }
);
session.error = errorMsg;
session.updatedAt = new Date();
persistSession(session, "error", errorMsg);
@@ -901,7 +919,7 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "AI processing failed";
console.error(`[milestone-slice-interview] Agent conversation error for session ${session.id}:`, err);
diagnostics.errorFromException("Agent conversation error for session", err, { sessionId: session.id, operation: "conversation" });
session.error = errorMessage;
session.updatedAt = new Date();
persistSession(session, "error", errorMessage);
@@ -955,7 +973,7 @@ export async function createTargetInterviewSession(
// Initialize AI agent in background
initializeAgent(session, rootDir).catch((err) => {
console.error(`[milestone-slice-interview] Failed to initialize agent for session ${sessionId}:`, err);
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
persistSession(session, "error", err.message || "Failed to initialize AI agent");
milestoneSliceInterviewStreamManager.broadcast(sessionId, {
type: "error",
@@ -1104,7 +1122,7 @@ export function getTargetInterviewSession(sessionId: string): TargetInterviewSes
sessions.set(restored.id, restored);
return restored;
} catch (error) {
console.error(`[milestone-slice-interview] Failed to restore session ${sessionId} from SQLite:`, error);
diagnostics.errorFromException("Failed to restore session from SQLite", error, { sessionId, operation: "restore" });
return undefined;
}
}
@@ -1267,4 +1285,7 @@ export function __resetMilestoneSliceInterviewState(): void {
}
_aiSessionDeletedListener = undefined;
_aiSessionStore = undefined;
// Reset diagnostics sink to default
resetDiagnosticsSink();
}