diff --git a/docs/agents.md b/docs/agents.md index 687b78d3e0..0985a3e09e 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -479,7 +479,7 @@ The **Token Usage** panel in Agents view is derived from each agent's persisted Fusion exposes cache-hit metrics across logs, API, and CLI: -- **Structured logs:** `token-cache-metrics` channel emits per-persist records with `taskId`, `agentId`, `role`, `inputTokens`, `cachedTokens`, `cacheWriteTokens`, and computed `hitRatio`. +- **Structured logs:** `token-cache-metrics` channel emits per-persist records with `taskId`, `agentId`, `role`, `inputTokens`, `cachedTokens`, `cacheWriteTokens`, and computed `hitRatio`. Emission is `debug`-gated (`FUSION_DEBUG=token-cache-metrics` or `FUSION_DEBUG=1`); it is off in the default TUI log pane. - **Agent API:** `GET /api/agents/:id/token-usage` returns `last24h`, `last7d`, and `allTime` window summaries for permanent agents. - **CLI rollup:** run `pnpm fn:cache-stats` (or `pnpm fn:cache-stats --json`) for project-wide role totals plus per-permanent-agent cache-hit summaries. diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 8b23b15fca..1a0601ba6f 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -21,7 +21,7 @@ FUSION_DEBUG=1 # everything (also: true, all, *) The variable is re-read per call, so it can be toggled on a long-lived process without recreating loggers. Debug lines emit under the `info` severity marker and render like any other info line. -Currently debug-gated classes include local/default routing, capacity and re-entrancy skips, poll/sweep no-actions, per-step success/progress, optional integration probes, and successful verification bookkeeping. State-changing recovery and dispatch outcomes remain visible. +Currently debug-gated classes include local/default routing, capacity and re-entrancy skips, poll/sweep no-actions, per-step success/progress, optional integration probes, successful verification bookkeeping, per-session agent setup (`agent-session` runtime/fallback resolution, planning mode, stuck-detector track bookkeeping), intentional skill-exclusion notices (`[skills] info: … disabled by project execution settings`), expected-missing PROMPT.md seed reads (ENOENT), token-cache metrics JSON, duplicate runtime `Specifying …` echoes, and zero-count recovery summaries. State-changing recovery and dispatch outcomes remain visible. Dashboard server code uses the core logger only: `import { createLogger } from "@fusion/core";`. Do not import an engine logger, use a relative cross-package logger path, or add a dashboard-local logger implementation. diff --git a/packages/engine/src/__tests__/agent-skills-flow.test.ts b/packages/engine/src/__tests__/agent-skills-flow.test.ts index 3c6b785c35..69b44baa4d 100644 --- a/packages/engine/src/__tests__/agent-skills-flow.test.ts +++ b/packages/engine/src/__tests__/agent-skills-flow.test.ts @@ -140,19 +140,19 @@ describe("agent skills flow - full integration", () => { expect(overrideResult.skills).toHaveLength(1); expect(overrideResult.skills[0].name).toBe("review"); - // Step 9: Verify warning diagnostic for disabled lint skill - const disabledLintWarning = overrideResult.diagnostics.find(d => + // Step 9: Intentional exclusion is an info diagnostic (debug-gated), not a warning + const disabledLintDiagnostic = overrideResult.diagnostics.find(d => d.message.includes("disabled") && d.message.includes("lint") ); - expect(disabledLintWarning).toBeDefined(); - expect(disabledLintWarning?.type).toBe("warning"); + expect(disabledLintDiagnostic).toBeDefined(); + expect(disabledLintDiagnostic?.type).toBe("info"); - // Step 10: Verify structured logger warning was called with disabled skill warning - const loggedMessages = mockPiLog.warn.mock.calls.map(c => c[0] as string); - const hasDisabledLintWarning = loggedMessages.some(m => + // Step 10: Emission goes to piLog.debug, not warn + const loggedMessages = mockPiLog.debug.mock.calls.map(c => c[0] as string); + const hasDisabledLintNotice = loggedMessages.some(m => m.includes("disabled") && m.includes("lint") ); - expect(hasDisabledLintWarning).toBe(true); + expect(hasDisabledLintNotice).toBe(true); }); it("flow with no exclusion pattern - both review and lint requested", async () => { diff --git a/packages/engine/src/__tests__/log-severity-manifest.ts b/packages/engine/src/__tests__/log-severity-manifest.ts index fb9d638e90..8422da0418 100644 --- a/packages/engine/src/__tests__/log-severity-manifest.ts +++ b/packages/engine/src/__tests__/log-severity-manifest.ts @@ -86,4 +86,15 @@ export const logSeverityManifest: SeverityManifestEntry[] = [ { pkg: "dashboard", file: "terminal-service.ts", anchor: "Working directory does not exist: ${cwd}", priorSeverity: "warn", severity: "debug" }, { pkg: "dashboard", file: "terminal-service.ts", anchor: "terminalLog.debug(`Session ${sessionId} not found`);", priorSeverity: "warn", severity: "debug" }, { pkg: "dashboard", file: "terminal-service.ts", anchor: "Session ${sessionId} not found for resize", priorSeverity: "warn", severity: "debug" }, + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + TUI flood demotions from operator log review. Anchors must share a source line with + `.debug(` (manifest is line-scoped). Multi-line sessionLog.debug setups, Tracking-task + triple sites, and zero-recovery gates are pinned in the spam-contract suite instead. + */ + { pkg: "engine", file: "triage.ts", anchor: "planLog.debug(`${task.id}: planning in ${leanPlanning ? \"fast\" : \"standard\"} mode`)", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "session-token-usage.ts", anchor: "cacheMetricsLog.debug(JSON.stringify({", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "executor.ts", anchor: "tokenCacheMetricsLog.debug(JSON.stringify({", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "runtimes/in-process-runtime.ts", anchor: "runtimeLog.debug(`Specifying ${t.id}...`)", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "triage.ts", anchor: "planLog.debug(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`)", priorSeverity: "warn", severity: "debug" }, ]; diff --git a/packages/engine/src/__tests__/log-severity-spam-contract.test.ts b/packages/engine/src/__tests__/log-severity-spam-contract.test.ts index 94663a1e84..ef22b6eadb 100644 --- a/packages/engine/src/__tests__/log-severity-spam-contract.test.ts +++ b/packages/engine/src/__tests__/log-severity-spam-contract.test.ts @@ -129,6 +129,53 @@ describe("log severity spam contract (source)", () => { expect(triage).not.toMatch(/planLog\.log\(`\$\{task\.id\}: using model \$\{modelDesc\}`\)/); }); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Operator TUI review: replan-session setup chatter, track bookkeeping, intentional skill + exclusions, zero-recovery no-ops, and metrics JSON must stay off default info/warn. + */ + it("session setup, track bookkeeping, skill exclusion, and zero-recovery stay debug-gated", () => { + const session = readSrc("agent-session-helpers.ts"); + const stuck = readSrc("stuck-task-detector.ts"); + const triage = readSrc("triage.ts"); + const resolver = readSrc("skill-resolver.ts"); + const selfHealing = readSrc("self-healing.ts"); + const mission = readSrc("mission-execution-loop.ts"); + const runtime = readSrc("runtimes/in-process-runtime.ts"); + const tokenUsage = readSrc("session-token-usage.ts"); + const executor = readSrc("executor.ts"); + + expect(session).toMatch(/sessionLog\.debug\(\s*`\[\$\{sessionPurpose\}\] grok-cli fallback/); + expect(session).toMatch(/sessionLog\.debug\(\s*`\[\$\{sessionPurpose\}\] Using runtime/); + expect(session).not.toMatch(/sessionLog\.log\(\s*`\[\$\{sessionPurpose\}\] grok-cli fallback/); + expect(session).not.toMatch(/sessionLog\.log\(\s*`\[\$\{sessionPurpose\}\] Using runtime/); + + expect(stuck).toMatch(/stuckLog\.debug\(`Tracking task \$\{trackingKey\}/); + expect(stuck).not.toMatch(/stuckLog\.log\(`Tracking task \$\{trackingKey\}/); + + expect(triage).toMatch(/planLog\.debug\(`\$\{task\.id\}: planning in \$\{leanPlanning/); + expect(triage).not.toMatch(/planLog\.log\(`\$\{task\.id\}: planning in \$\{leanPlanning/); + expect(triage).toMatch(/if \(code === "ENOENT"\) \{\s*planLog\.debug\(`\$\{taskId\}: failed to read PROMPT\.md/); + expect(triage).toMatch(/planLog\.warn\(`\$\{taskId\}: failed to read PROMPT\.md during \$\{context\}/); + + expect(resolver).toMatch(/exists but is disabled by project execution settings/); + expect(resolver).toMatch(/type: "info" as ResourceDiagnostic\["type"\],\s*message: `Skill at '\$\{excludedPath\}' exists but is disabled/); + + expect(selfHealing).toMatch(/if \(clearedAgentIds\.size > 0\) \{\s*log\.log\(`Recovered \$\{clearedAgentIds\.size\} drifted/); + expect(selfHealing).toMatch(/log\.debug\(`Recovered \$\{clearedAgentIds\.size\} drifted durable agent task link/); + + expect(mission).toMatch(/if \(recoveredCount > 0\) \{\s*loopLog\.log\(`Active mission recovery complete/); + expect(mission).toMatch(/loopLog\.debug\(`Active mission recovery complete: recovered \$\{recoveredCount\} features`\)/); + + expect(runtime).toMatch(/runtimeLog\.debug\(`Specifying \$\{t\.id\}\.\.\.`\)/); + expect(runtime).not.toMatch(/runtimeLog\.log\(`Specifying \$\{t\.id\}\.\.\.`\)/); + + expect(tokenUsage).toMatch(/cacheMetricsLog\.debug\(JSON\.stringify\(/); + expect(tokenUsage).not.toMatch(/cacheMetricsLog\.log\(JSON\.stringify\(/); + expect(executor).toMatch(/tokenCacheMetricsLog\.debug\(JSON\.stringify\(/); + expect(executor).not.toMatch(/tokenCacheMetricsLog\.log\(JSON\.stringify\(/); + }); + it("self-healing no-action/skip, worktree-pool probes, and ntfy bookkeeping use debug", () => { const sh = readSrc("self-healing.ts"); const wt = readSrc("worktree-pool.ts"); diff --git a/packages/engine/src/__tests__/session-token-usage.test.ts b/packages/engine/src/__tests__/session-token-usage.test.ts index 4acc0c4e19..3c6bf075de 100644 --- a/packages/engine/src/__tests__/session-token-usage.test.ts +++ b/packages/engine/src/__tests__/session-token-usage.test.ts @@ -55,33 +55,40 @@ describe("accumulateSessionTokenUsage", () => { }); it("writes initial token usage and emits cache metrics log", async () => { + const prevDebug = process.env.FUSION_DEBUG; + process.env.FUSION_DEBUG = "token-cache-metrics"; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const store = createStore(undefined); - const session = createSession({ tokens: { input: 100, output: 30, cacheRead: 5, cacheWrite: 2 } }); + try { + const store = createStore(undefined); + const session = createSession({ tokens: { input: 100, output: 30, cacheRead: 5, cacheWrite: 2 } }); - await accumulateSessionTokenUsage(store, "FN-1", session, { agentId: "agent-1", role: "reviewer" }); + await accumulateSessionTokenUsage(store, "FN-1", session, { agentId: "agent-1", role: "reviewer" }); - expect(store.updateTask).toHaveBeenCalledTimes(1); - const call = store.updateTask.mock.calls[0]![1] as { tokenUsage: Task["tokenUsage"] }; - expect(call.tokenUsage).toMatchObject({ - inputTokens: 100, - outputTokens: 30, - cachedTokens: 5, - cacheWriteTokens: 2, - totalTokens: 137, - }); - const cacheLogCall = errorSpy.mock.calls.find((entry) => String(entry[0]).includes("[token-cache-metrics]")); - expect(cacheLogCall).toBeTruthy(); - const payload = JSON.parse(String(cacheLogCall?.[0] ?? "").replace(/^.*\[token-cache-metrics\]\s*/, "")); - expect(payload).toMatchObject({ - taskId: "FN-1", - agentId: "agent-1", - role: "reviewer", - inputTokens: 100, - cachedTokens: 5, - cacheWriteTokens: 2, - hitRatio: computeCacheHitRatio(100, 5), - }); + expect(store.updateTask).toHaveBeenCalledTimes(1); + const call = store.updateTask.mock.calls[0]![1] as { tokenUsage: Task["tokenUsage"] }; + expect(call.tokenUsage).toMatchObject({ + inputTokens: 100, + outputTokens: 30, + cachedTokens: 5, + cacheWriteTokens: 2, + totalTokens: 137, + }); + const cacheLogCall = errorSpy.mock.calls.find((entry) => String(entry[0]).includes("[token-cache-metrics]")); + expect(cacheLogCall).toBeTruthy(); + const payload = JSON.parse(String(cacheLogCall?.[0] ?? "").replace(/^.*\[token-cache-metrics\]\s*/, "")); + expect(payload).toMatchObject({ + taskId: "FN-1", + agentId: "agent-1", + role: "reviewer", + inputTokens: 100, + cachedTokens: 5, + cacheWriteTokens: 2, + hitRatio: computeCacheHitRatio(100, 5), + }); + } finally { + if (prevDebug === undefined) delete process.env.FUSION_DEBUG; + else process.env.FUSION_DEBUG = prevDebug; + } }); it("uses an explicit task-start baseline to exclude resumed-session lifetime tokens", async () => { @@ -220,23 +227,30 @@ describe("accumulateSessionTokenUsage", () => { }); it("emits token-cache-metrics log when executor persists non-zero delta", async () => { + const prevDebug = process.env.FUSION_DEBUG; + process.env.FUSION_DEBUG = "token-cache-metrics"; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const store = createStore(undefined); - const executor = Object.create(TaskExecutor.prototype) as any; - executor.store = store; - executor.tokenUsageBaselines = new Map(); - executor.activeSessions = new Map(); - executor.currentRunContexts = new Map(); + try { + const store = createStore(undefined); + const executor = Object.create(TaskExecutor.prototype) as any; + executor.store = store; + executor.tokenUsageBaselines = new Map(); + executor.activeSessions = new Map(); + executor.currentRunContexts = new Map(); - await executor.persistTokenUsage("FN-1", { - getSessionStats: () => ({ tokens: { input: 3, output: 2, cacheRead: 1, cacheWrite: 0, total: 6 } }), - model: { provider: "mock", id: "scripted" }, - }); + await executor.persistTokenUsage("FN-1", { + getSessionStats: () => ({ tokens: { input: 3, output: 2, cacheRead: 1, cacheWrite: 0, total: 6 } }), + model: { provider: "mock", id: "scripted" }, + }); - const cacheLogCall = errorSpy.mock.calls.find((entry) => String(entry[0]).includes("[token-cache-metrics]")); - expect(cacheLogCall).toBeTruthy(); - const call = store.updateTask.mock.calls[0]![1] as { tokenUsage: Task["tokenUsage"] }; - expect(call.tokenUsage).toMatchObject({ modelProvider: "mock", modelId: "scripted" }); + const cacheLogCall = errorSpy.mock.calls.find((entry) => String(entry[0]).includes("[token-cache-metrics]")); + expect(cacheLogCall).toBeTruthy(); + const call = store.updateTask.mock.calls[0]![1] as { tokenUsage: Task["tokenUsage"] }; + expect(call.tokenUsage).toMatchObject({ modelProvider: "mock", modelId: "scripted" }); + } finally { + if (prevDebug === undefined) delete process.env.FUSION_DEBUG; + else process.env.FUSION_DEBUG = prevDebug; + } }); it("enforces soft and hard budgets through the real persist helper exactly once", async () => { diff --git a/packages/engine/src/__tests__/skill-resolver.test.ts b/packages/engine/src/__tests__/skill-resolver.test.ts index a511b775f3..f1d7bc736f 100644 --- a/packages/engine/src/__tests__/skill-resolver.test.ts +++ b/packages/engine/src/__tests__/skill-resolver.test.ts @@ -689,7 +689,8 @@ describe("createSkillsOverrideFromSelection", () => { expect(result.skills).toHaveLength(0); expect(result.diagnostics).toHaveLength(1); - expect(result.diagnostics[0].type).toBe("warning"); + // Intentional exclusions are info diagnostics (debug-gated emission), not warn. + expect(result.diagnostics[0].type).toBe("info"); expect(result.diagnostics[0].message).toContain("disabled by project execution settings"); expect(result.diagnostics.some((diagnostic) => diagnostic.message.includes("not found"))).toBe(false); }); @@ -791,7 +792,7 @@ describe("createSkillsOverrideFromSelection", () => { expect(lastCall).toContain("missing-skill"); }); - it("produces warning diagnostic for disabled skills (exists but excluded by patterns)", () => { + it("produces info diagnostic for disabled skills (exists but excluded by patterns)", () => { // Simulate a skill that exists but was disabled by project exclusion pattern const selection: SkillSelectionResult = { allowedSkillPaths: new Set(), @@ -817,15 +818,16 @@ describe("createSkillsOverrideFromSelection", () => { // Skill should be filtered out (excluded) expect(result.skills).toHaveLength(0); - // Should produce warning diagnostic for disabled skill (ResourceDiagnostic only supports warning|error|collision) + // Intentional exclusions are info (debug-gated emission), not operator warnings expect(result.diagnostics).toHaveLength(1); - expect(result.diagnostics[0].type).toBe("warning"); + expect(result.diagnostics[0].type).toBe("info"); expect(result.diagnostics[0].message).toContain("disabled"); expect(result.diagnostics[0].message).toContain("disabled-skill"); - // Verify logging - expect(mockPiLog.warn).toHaveBeenCalled(); - const lastCall = mockPiLog.warn.mock.calls[mockPiLog.warn.mock.calls.length - 1][0] as string; + // Verify logging routes to debug, not warn + expect(mockPiLog.warn).not.toHaveBeenCalled(); + expect(mockPiLog.debug).toHaveBeenCalled(); + const lastCall = mockPiLog.debug.mock.calls[mockPiLog.debug.mock.calls.length - 1][0] as string; expect(lastCall).toContain("disabled"); }); @@ -1224,7 +1226,7 @@ describe("createSkillsOverrideFromSelection", () => { expect(result.skills).toEqual([]); expect(result.diagnostics).toContainEqual(expect.objectContaining({ - type: "warning", + type: "info", message: expect.stringContaining("disabled by project execution settings"), })); }); diff --git a/packages/engine/src/agent-session-helpers.ts b/packages/engine/src/agent-session-helpers.ts index 5e0fda0ce8..94531a9b54 100644 --- a/packages/engine/src/agent-session-helpers.ts +++ b/packages/engine/src/agent-session-helpers.ts @@ -984,7 +984,13 @@ export async function createResolvedAgentSession( `[${sessionPurpose}] configured grok-cli fallback "${runtimeOptions.fallbackModelId ?? "unknown"}" dropped: no Fusion-visible GROK_API_KEY and the Grok CLI runtime plugin is unavailable; primary "${runtimeOptions.defaultProvider}/${runtimeOptions.defaultModelId}" is unchanged. Install/enable the Grok CLI runtime plugin or set GROK_API_KEY.`, ); } else if (deferredGrokFallback) { - sessionLog.log( + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Deferred grok-cli fallback is the expected no-visible-key config path and fires on every + session create. Real engagement already warns + audits `session:grok-cli-fallback-engaged`; + keep the deferral notice on debug (FUSION_DEBUG=agent-session) so the TUI is not flooded. + */ + sessionLog.debug( `[${sessionPurpose}] grok-cli fallback "${deferredGrokFallback.modelId ?? "unknown"}" deferred to the Grok CLI runtime: it engages only if primary "${runtimeOptions.defaultProvider}/${runtimeOptions.defaultModelId}" fails with a retryable model error.`, ); } @@ -997,7 +1003,12 @@ export async function createResolvedAgentSession( } : await resolveRuntime(buildRuntimeResolutionContext(sessionPurpose, pluginRunner, effectiveRuntimeHint)); - sessionLog.log( + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Runtime resolution is per-session setup chatter (same class as demoted planning `using model`). + Audit already records `session:runtime-resolved`; keep the TUI line on debug (FUSION_DEBUG=agent-session). + */ + sessionLog.debug( `[${sessionPurpose}] Using runtime "${resolved.runtimeId}" (configured=${resolved.wasConfigured})`, ); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 17d9c41cf5..ccbec31cdd 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -5145,7 +5145,12 @@ export class TaskExecutor { if (!merged) return; const tokenUsage = this.tokenUsageWithModelSnapshot(merged, activeSession, task.tokenUsage, delta); - tokenCacheMetricsLog.log(JSON.stringify({ + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Executor token-cache metrics mirror session-token-usage: debug-only telemetry + (FUSION_DEBUG=token-cache-metrics), not default TUI noise. + */ + tokenCacheMetricsLog.debug(JSON.stringify({ taskId, agentId: task.assignedAgentId ?? undefined, role: "executor", diff --git a/packages/engine/src/mission-execution-loop.ts b/packages/engine/src/mission-execution-loop.ts index 4e2e5263fb..77cee275ee 100644 --- a/packages/engine/src/mission-execution-loop.ts +++ b/packages/engine/src/mission-execution-loop.ts @@ -477,7 +477,16 @@ export class MissionExecutionLoop extends EventEmitter { } } - loopLog.log(`Active mission recovery complete: recovered ${recoveredCount} features`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Zero-feature recovery complete is a startup no-op — debug only (FUSION_DEBUG=mission-loop). + Non-zero recoveries stay on log as operator-visible state repairs. + */ + if (recoveredCount > 0) { + loopLog.log(`Active mission recovery complete: recovered ${recoveredCount} features`); + } else { + loopLog.debug(`Active mission recovery complete: recovered ${recoveredCount} features`); + } return { recoveredCount }; } catch (err) { loopLog.error("Error during active mission recovery:", err); diff --git a/packages/engine/src/runtimes/in-process-runtime.ts b/packages/engine/src/runtimes/in-process-runtime.ts index fbe9621341..1215ef8f50 100644 --- a/packages/engine/src/runtimes/in-process-runtime.ts +++ b/packages/engine/src/runtimes/in-process-runtime.ts @@ -1559,7 +1559,12 @@ export class InProcessRuntime acquirePlanningWorktree: (taskId) => this.executor.ensureTaskWorktreeForPlanning(taskId), onSpecifyStart: (t) => { this.recordActivity(); - runtimeLog.log(`Specifying ${t.id}...`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Duplicate of triage's richer `Specifying ${id}: ${title}` planLog line. Keep this + short runtime echo on debug (FUSION_DEBUG=runtime) so planning start is not double-logged. + */ + runtimeLog.debug(`Specifying ${t.id}...`); }, onSpecifyComplete: (t, report) => { // Activity is recorded for EVERY outcome: a planning session ran either diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index b263860256..e97c38f70b 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -13355,7 +13355,16 @@ const movedTask = await this.store.moveTask(task.id, completeLane); log.log(`Cleared drifted durable agent task link for ${agent.id} (${linkedTaskId}): ${reason}; file-scope lease preserved when present`); } - log.log(`Recovered ${clearedAgentIds.size} drifted durable agent task link(s)`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Zero-recovery summary is a no-op sweep result — debug only (FUSION_DEBUG=self-healing). + Non-zero recoveries stay on log so operators still see real link repairs. + */ + if (clearedAgentIds.size > 0) { + log.log(`Recovered ${clearedAgentIds.size} drifted durable agent task link(s)`); + } else { + log.debug(`Recovered ${clearedAgentIds.size} drifted durable agent task link(s)`); + } return clearedAgentIds.size; } diff --git a/packages/engine/src/session-token-usage.ts b/packages/engine/src/session-token-usage.ts index dbbec5684c..b75fa3f5dc 100644 --- a/packages/engine/src/session-token-usage.ts +++ b/packages/engine/src/session-token-usage.ts @@ -167,7 +167,12 @@ export async function accumulateSessionTokenUsage( }, model, now), }; - cacheMetricsLog.log(JSON.stringify({ + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Per-persist token cache metrics are structured telemetry, not operator state changes. + Gate on FUSION_DEBUG=token-cache-metrics so the TUI is not filled with JSON every session end. + */ + cacheMetricsLog.debug(JSON.stringify({ taskId, agentId: options?.agentId, role, diff --git a/packages/engine/src/skill-resolver.ts b/packages/engine/src/skill-resolver.ts index 3133fae2b6..e1222b8fed 100644 --- a/packages/engine/src/skill-resolver.ts +++ b/packages/engine/src/skill-resolver.ts @@ -519,8 +519,14 @@ export function createSkillsOverrideFromSelection( for (const excludedPath of excludedSkillPaths) { if (hasDiscoveredMatch(excludedPath)) { + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Intentional project skill exclusions are expected config, not operator degradation. + Use type=info so emission paths route to piLog.debug (FUSION_DEBUG=pi) instead of + warn-flooding the TUI on every session that rediscovers the same disabled skill. + */ newDiagnostics.push({ - type: "warning", + type: "info" as ResourceDiagnostic["type"], message: `Skill at '${excludedPath}' exists but is disabled by project execution settings${purpose}`, path: excludedPath, }); diff --git a/packages/engine/src/stuck-task-detector.ts b/packages/engine/src/stuck-task-detector.ts index 72029d86e6..4602e26dad 100644 --- a/packages/engine/src/stuck-task-detector.ts +++ b/packages/engine/src/stuck-task-detector.ts @@ -338,19 +338,30 @@ export class StuckTaskDetector { } this.exhaustedTasks.delete(canonicalId); this.tracked.set(trackingKey, emptyTrackedTask(session, Date.now(), canonicalId)); - stuckLog.log(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Track/untrack bookkeeping fires on every session start and flooded the TUI during + replan storms. Keep on debug (FUSION_DEBUG=stuck-detector); real stuck detections + and terminal-skip/error paths stay on log/warn/error. + */ + stuckLog.debug(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); }) .catch((err) => { stuckLog.error(`Failed to validate exhausted status for ${canonicalId}; proceeding to track:`, err); this.exhaustedTasks.delete(canonicalId); this.tracked.set(trackingKey, emptyTrackedTask(session, Date.now(), canonicalId)); - stuckLog.log(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); + stuckLog.debug(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); }); return; } this.tracked.set(trackingKey, emptyTrackedTask(session, Date.now(), canonicalId)); - stuckLog.log(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Track bookkeeping is steady-state session lifecycle chatter — debug only + (FUSION_DEBUG=stuck-detector). Stuck detections remain log/warn/error. + */ + stuckLog.debug(`Tracking task ${trackingKey} (canonical=${canonicalId}, total tracked: ${this.tracked.size})`); } /** diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 2595dffa23..fbbd54f349 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -1449,7 +1449,18 @@ export class TriageProcessor { const promptPath = join(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md"); const written = await readFile(promptPath, "utf-8").catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); - planLog.warn(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`); + const code = err && typeof err === "object" && "code" in err ? String((err as { code?: unknown }).code) : undefined; + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + needs-replan revision seed commonly has no PROMPT.md yet (ENOENT) — that is an expected + cold/fresh respec path, not operator degradation. Demote missing-file to debug + (FUSION_DEBUG=plan); keep warn for unexpected I/O so real disk failures stay visible. + */ + if (code === "ENOENT") { + planLog.debug(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`); + } else { + planLog.warn(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`); + } return ""; }); return written.trim().length > 0 ? written : undefined; @@ -2589,7 +2600,12 @@ export class TriageProcessor { planLog.warn(`${task.id}: failed to resolve triage agent instructions, continuing with defaults: ${msg}`); } } - planLog.log(`${task.id}: planning in ${leanPlanning ? "fast" : "standard"} mode`); + /* + FNXC:EngineDiagnostics 2026-08-01-18:11: + Lean vs standard planning mode is config-derived setup and fires every planning attempt. + Same flood class as demoted `using model` — keep on debug (FUSION_DEBUG=plan). + */ + planLog.debug(`${task.id}: planning in ${leanPlanning ? "fast" : "standard"} mode`); const triageIdentitySection = assignedAgent ? `## Identity\n\nYou are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.title.trim()}` : ""} (agent ID: ${assignedAgent.id}, role: ${assignedAgent.role}).` : "";