fix: demote high-frequency TUI log lines to debug

Session setup, track bookkeeping, intentional skill exclusions, token-cache
metrics, zero-count recovery summaries, and expected-missing PROMPT seed reads
were flooding the default log pane. Gate them behind FUSION_DEBUG so only
state transitions and operator-actionable warnings remain visible.
This commit is contained in:
gsxdsm
2026-08-01 11:48:56 -07:00
parent 7dbcff139c
commit 60706ed5e4
16 changed files with 220 additions and 69 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 () => {

View File

@@ -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" },
];

View File

@@ -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");

View File

@@ -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 () => {

View File

@@ -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<string>(),
@@ -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"),
}));
});

View File

@@ -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})`,
);

View File

@@ -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",

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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})`);
}
/**

View File

@@ -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}).`
: "";