diff --git a/src/functions/compress.ts b/src/functions/compress.ts index 0569555e0..de2d8ca73 100644 --- a/src/functions/compress.ts +++ b/src/functions/compress.ts @@ -21,6 +21,7 @@ import { validateOutput } from "../eval/validator.js"; import { scoreCompression } from "../eval/quality.js"; import { compressWithRetry } from "../eval/self-correct.js"; import type { MetricsStore } from "../eval/metrics-store.js"; +import { buildSyntheticCompression } from "./compress-synthetic.js"; import { logger } from "../logger.js"; const VALID_TYPES = new Set([ @@ -78,6 +79,90 @@ export function registerCompressFunction( }) => { const startMs = Date.now(); + if (provider.name === "noop") { + logger.info("Compression skipped (noop provider) — generating synthetic compression", { + obsId: data.observationId, + }); + const synthetic = buildSyntheticCompression(data.raw); + synthetic.id = data.observationId; + synthetic.sessionId = data.sessionId; + if (data.raw.timestamp) { + synthetic.timestamp = data.raw.timestamp; + } + + await kv.set( + KV.observations(data.sessionId), + data.observationId, + synthetic, + ); + + try { + getSearchIndex().add(synthetic); + } catch (err) { + logger.warn("Failed to index compressed observation into BM25", { + obsId: synthetic.id, + sessionId: synthetic.sessionId, + title: synthetic.title, + error: err instanceof Error ? err.message : String(err), + }); + } + + await vectorIndexAddGuarded( + synthetic.id, + synthetic.sessionId, + synthetic.title + " " + (synthetic.narrative || ""), + { kind: "observation", logId: synthetic.id }, + ); + + const streamResults = await Promise.allSettled([ + sdk.trigger({ + function_id: "stream::set", + payload: { + stream_name: STREAM.name, + group_id: STREAM.group(data.sessionId), + item_id: data.observationId, + data: { type: "compressed", observation: synthetic }, + }, + }), + sdk.trigger({ + function_id: "stream::send", + payload: { + stream_name: STREAM.name, + group_id: STREAM.viewerGroup, + id: `compressed-${data.observationId}`, + type: "compressed_observation", + data: { + type: "compressed", + observation: synthetic, + sessionId: data.sessionId, + }, + }, + action: TriggerAction.Void(), + }), + ]); + + for (const res of streamResults) { + if (res.status === "rejected") { + logger.warn("Non-fatal stream publish failure after compress", { + sessionId: data.sessionId, + observationId: data.observationId, + error: + res.reason instanceof Error + ? res.reason.message + : String(res.reason), + }); + } + } + + logger.info("Observation compressed (synthetic noop fallback)", { + obsId: data.observationId, + type: synthetic.type, + importance: synthetic.importance, + }); + + return { success: true, compressed: synthetic, qualityScore: synthetic.confidence * 100 }; + } + let imageDescription: string | undefined; const hasImage = data.raw.modality === "image" || data.raw.modality === "mixed"; diff --git a/test/auto-compress.test.ts b/test/auto-compress.test.ts index fdcced8fe..0c6fbe13d 100644 --- a/test/auto-compress.test.ts +++ b/test/auto-compress.test.ts @@ -79,10 +79,10 @@ describe("mem::observe auto-compress gate (#138)", () => { // test that sets the env var can be undermined by cached module // state from an earlier test (and vice versa). vi.resetModules(); - delete process.env["AGENTMEMORY_AUTO_COMPRESS"]; + process.env["AGENTMEMORY_AUTO_COMPRESS"] = "false"; }); afterEach(() => { - delete process.env["AGENTMEMORY_AUTO_COMPRESS"]; + process.env["AGENTMEMORY_AUTO_COMPRESS"] = "false"; }); it("default (AGENTMEMORY_AUTO_COMPRESS unset): does NOT fire mem::compress", async () => { @@ -244,3 +244,60 @@ describe("buildSyntheticCompression", () => { expect(synth.type).toBe("error"); }); }); + +describe("mem::compress noop provider graceful fallback", () => { + it("gracefully falls back to synthetic compression when provider is noop", async () => { + const { registerCompressFunction } = await import( + "../src/functions/compress.js" + ); + const sdk = mockSdk(); + const kv = mockKV(); + const mockProvider = { + name: "noop", + compress: vi.fn().mockResolvedValue(""), + summarize: vi.fn().mockResolvedValue(""), + }; + const mockMetricsStore = { + record: vi.fn(), + }; + + registerCompressFunction( + sdk as any, + kv as any, + mockProvider as any, + mockMetricsStore as any + ); + + const rawObs: RawObservation = { + id: "obs_test_noop", + sessionId: "ses_test_noop", + timestamp: new Date().toISOString(), + hookType: "post_tool_use", + toolName: "Read", + toolInput: { file_path: "src/noop-test.ts" }, + toolOutput: "test contents", + raw: {}, + }; + + const compressFn = sdk.fns.get("mem::compress"); + expect(compressFn).toBeDefined(); + + const result = await compressFn({ + observationId: "obs_test_noop", + sessionId: "ses_test_noop", + raw: rawObs, + }); + + expect(result.success).toBe(true); + expect(result.compressed).toBeDefined(); + expect(result.compressed.type).toBe("file_read"); + expect(result.compressed.title).toBe("Read"); + expect(result.compressed.files).toContain("src/noop-test.ts"); + + expect(mockMetricsStore.record).not.toHaveBeenCalled(); + + const stored = await kv.get("mem:obs:ses_test_noop", "obs_test_noop"); + expect(stored).toBeDefined(); + expect((stored as any).type).toBe("file_read"); + }); +});