feat(FN-2083): merge fusion/fn-2083
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user