feat(FN-2083): merge fusion/fn-2083

This commit is contained in:
gsxdsm
2026-04-18 20:30:50 -07:00
parent f2e5f803ea
commit d559e790a2
2 changed files with 144 additions and 6 deletions

View File

@@ -344,4 +344,116 @@ describe("AgentLogger", () => {
await vi.advanceTimersByTimeAsync(0);
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-018", "Bash", "tool_result", undefined, "merger");
});
describe("persistence failure observability", () => {
const warningFor = (warnSpy: ReturnType<typeof vi.spyOn>): string => {
return warnSpy.mock.calls.map((call) => call.map((arg) => String(arg)).join(" ")).join("\n");
};
it("text flush failure logs structured warning", async () => {
const store = {
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
} as unknown as TaskStore;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const logger = new AgentLogger({
store,
taskId: "FN-2083-TEXT",
flushSizeBytes: 1,
});
logger.onText("some text");
await vi.advanceTimersByTimeAsync(0);
const warning = warningFor(warnSpy);
expect(warning).toContain("[agent-logger]");
expect(warning).toContain("FN-2083-TEXT");
expect(warning).toContain("ENOSPC");
warnSpy.mockRestore();
});
it("thinking flush failure logs structured warning", async () => {
const store = {
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
} as unknown as TaskStore;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const logger = new AgentLogger({
store,
taskId: "FN-2083-THINK",
flushSizeBytes: 1,
});
logger.onThinking("deep thought");
await vi.advanceTimersByTimeAsync(0);
const warning = warningFor(warnSpy);
expect(warning).toContain("[agent-logger]");
expect(warning).toContain("FN-2083-THINK");
expect(warning).toContain("thinking");
expect(warning).toContain("ENOSPC");
warnSpy.mockRestore();
});
it("tool-start persistence failure logs structured warning", async () => {
const store = {
appendAgentLog: vi.fn().mockRejectedValue(new Error("EACCES: permission denied")),
} as unknown as TaskStore;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const logger = new AgentLogger({ store, taskId: "FN-2083-TOOL-START" });
logger.onToolStart("Bash", { command: "ls" });
await vi.advanceTimersByTimeAsync(0);
const warning = warningFor(warnSpy);
expect(warning).toContain("[agent-logger]");
expect(warning).toContain("FN-2083-TOOL-START");
expect(warning).toContain("Bash");
expect(warning).toContain("EACCES");
warnSpy.mockRestore();
});
it("tool-end persistence failure logs structured warning", async () => {
const store = {
appendAgentLog: vi.fn().mockRejectedValue(new Error("EPERM: operation not permitted")),
} as unknown as TaskStore;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const logger = new AgentLogger({ store, taskId: "FN-2083-TOOL-END" });
logger.onToolEnd("Bash", false, "output");
await vi.advanceTimersByTimeAsync(0);
const warning = warningFor(warnSpy);
expect(warning).toContain("[agent-logger]");
expect(warning).toContain("FN-2083-TOOL-END");
expect(warning).toContain("EPERM");
warnSpy.mockRestore();
});
it("flush() propagates text and thinking warnings", async () => {
const store = {
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
} as unknown as TaskStore;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const logger = new AgentLogger({
store,
taskId: "FN-2083-FLUSH",
flushSizeBytes: 1024,
});
logger.onText("text");
logger.onThinking("thought");
await logger.flush();
const warning = warningFor(warnSpy);
expect(warnSpy).toHaveBeenCalledTimes(2);
expect(warning).toContain("[agent-logger]");
expect(warning).toContain("FN-2083-FLUSH");
expect(warning).toContain("ENOSPC");
warnSpy.mockRestore();
});
});
});

View File

@@ -1,10 +1,13 @@
import type { TaskStore, AgentRole } from "@fusion/core";
import { createLogger } from "./logger.js";
/** Default byte threshold before an automatic flush. */
const FLUSH_SIZE_BYTES = 1024;
/** Default timer interval (ms) for periodic flush of small writes. */
const FLUSH_INTERVAL_MS = 500;
const log = createLogger("agent-logger");
/**
* Produce a human-readable summary from tool arguments.
* Returns the full argument value without truncation.
@@ -147,7 +150,14 @@ export class AgentLogger {
if (this.thinkingFlushTimer) { clearTimeout(this.thinkingFlushTimer); this.thinkingFlushTimer = null; }
this.flushThinkingBuffer();
const detail = summarizeToolArgs(name, args);
this.store.appendAgentLog(this.taskId, name, "tool", detail, this.agent).catch(() => {});
this.store.appendAgentLog(this.taskId, name, "tool", detail, this.agent).catch((err) => {
log.warn(
"Failed to persist tool-start log for task %s (tool: %s): %s",
this.taskId,
name,
err instanceof Error ? err.message : String(err),
);
});
}
/**
@@ -165,7 +175,15 @@ export class AgentLogger {
const str = typeof result === "string" ? result : JSON.stringify(result);
detail = str.length > 500 ? str.slice(0, 500) + "…" : str;
}
this.store.appendAgentLog(this.taskId, name, type, detail, this.agent).catch(() => {});
this.store.appendAgentLog(this.taskId, name, type, detail, this.agent).catch((err) => {
log.warn(
"Failed to persist tool-end log for task %s (tool: %s, type: %s): %s",
this.taskId,
name,
type,
err instanceof Error ? err.message : String(err),
);
});
}
/**
@@ -185,8 +203,12 @@ export class AgentLogger {
if (this.textBuffer.length === 0) return Promise.resolve();
const chunk = this.textBuffer;
this.textBuffer = "";
return this.store.appendAgentLog(this.taskId, chunk, "text", undefined, this.agent).catch(() => {
/* best-effort persistence */
return this.store.appendAgentLog(this.taskId, chunk, "text", undefined, this.agent).catch((err) => {
log.warn(
"Failed to persist text log for task %s: %s",
this.taskId,
err instanceof Error ? err.message : String(err),
);
});
}
@@ -194,8 +216,12 @@ export class AgentLogger {
if (this.thinkingBuffer.length === 0) return Promise.resolve();
const chunk = this.thinkingBuffer;
this.thinkingBuffer = "";
return this.store.appendAgentLog(this.taskId, chunk, "thinking", undefined, this.agent).catch(() => {
/* best-effort persistence */
return this.store.appendAgentLog(this.taskId, chunk, "thinking", undefined, this.agent).catch((err) => {
log.warn(
"Failed to persist thinking log for task %s: %s",
this.taskId,
err instanceof Error ? err.message : String(err),
);
});
}