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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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})`,
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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})`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}).`
|
||||
: "";
|
||||
|
||||
Reference in New Issue
Block a user