FN-9181: bound recall-capture audit emission
Route detached recall-capture telemetry through the bounded core audit seam. - Preserve the injectable audit adapter while bounding production recorded and failed events. - Add hostile-sink, metadata-contract, and source-routing regression coverage. - Document the class-A decision and add a patch changeset. Files changed: .changeset/fn-9181-recall-capture-bounded-audit.md | 7 +++ AGENTS.md | 1 + docs/run-audit.md | 4 +- .../core-run-audit-emitter-isolation.test.ts | 3 +- .../__tests__/core-run-audit-sink-health.test.ts | 62 ++++++++++++++++++- .../src/__tests__/memory/recall-capture.test.ts | 71 +++++++++++++++++++++- packages/core/src/memory/recall-capture.ts | 43 +++++++------ 7 files changed, 166 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-9181 Fusion-Task-Lineage: 4700caca-7182-4aa7-9355-b0bfdef0e4e1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-9181-recall-capture-bounded-audit.md
Normal file
7
.changeset/fn-9181-recall-capture-bounded-audit.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Prevent stalled recall telemetry from retaining detached memory captures.
|
||||
category: fix
|
||||
dev: Routes packages/core/src/memory/recall-capture.ts through emitBoundedRunAudit.
|
||||
@@ -295,6 +295,7 @@ canonical emitters remain explicit exclusions until their separately scoped hard
|
||||
- FNXC:RunAudit 2026-08-20-05:49: FN-9177 requires new core best-effort emitters to use `packages/core/src/run-audit/emit-bounded-run-audit.ts`. It deliberately mirrors the engine seam because core cannot import engine; transactional and deliberately awaited durability writers remain unbounded.
|
||||
- FNXC:RunAudit 2026-08-20-07:14: FN-9182 requires core emitters whose behavior branches on audit success to use `emitBoundedRunAuditWithOutcome`; `emitBoundedRunAudit` remains the default best-effort seam, while transactional and deliberately awaited durability writers remain unbounded.
|
||||
- FN-9180: The `task-deleted-outbox:*` catch-up, reconciliation-fallback, lease-fenced, and retention-pruned emitters use `packages/core/src/run-audit/emit-bounded-run-audit.ts`, remain awaited at their post-cursor/post-DELETE positions, and require hostile-sink production-path coverage.
|
||||
- FN-9181: Detached recall-capture `memory:capture-recorded` and `memory:capture-failed` emissions use the core bounded seam, preserving the injectable test adapter and preventing stalled telemetry from retaining detached capture work; see `docs/run-audit.md`.
|
||||
- FN-9109: `session:cross-runtime-fallback-engaged` records a single retryable-failure handoff from a primary runtime to a deferred CLI runtime. Metadata is ids/outcomes-only (`sessionPurpose`, primary/fallback provider and model IDs, trigger point, failure category, `contextTransferred`); never record error prose or transferred transcript text.
|
||||
|
||||
- FN-8958: `merge:orphan-write-fenced` is emitted once per orphan merge body at its fence's first interaction. Metadata is ids/counts/outcomes-only: `{ taskId, category, interaction, suppressedCount }`; `suppressedCount` is the emit-time count (`1` for `interaction:"suppressed"`, `0` for `interaction:"rejected"`), never a cumulative body total.
|
||||
|
||||
@@ -82,10 +82,10 @@ This applies to executor, run-auditor, self-healing, merger, PR reconciliation,
|
||||
|
||||
### Core emit-seam policy
|
||||
|
||||
Core best-effort emitters use `packages/core/src/run-audit/emit-bounded-run-audit.ts`. This is a deliberate copy of the engine seam because `@fusion/core` cannot import `@fusion/engine`; it synchronously invokes a valid sink, then absorbs throws, rejection, timeout, and late settlement without making telemetry lifecycle-load-bearing. `emitBoundedRunAudit` is the default void seam. `emitBoundedRunAuditWithOutcome` returns `recorded`, `absent`, `failed` (with the original error), or `timed-out` where a forensic throw ordering or caller-visible skipped payload depends on the audit result; workflow-switch torn reconciliation and phantom committed-reservation reconciliation use it. Transactional writers and explicitly awaited durability/ordering writers remain unbounded. `packages/core/src/__tests__/core-run-audit-sink-health.test.ts` and `core-run-audit-emitter-isolation.test.ts` respectively enforce hostile-sink behavior and source routing.
|
||||
Core best-effort emitters use `packages/core/src/run-audit/emit-bounded-run-audit.ts`. This is a deliberate copy of the engine seam because `@fusion/core` cannot import `@fusion/engine`; it synchronously invokes a valid sink, then absorbs throws, rejection, timeout, and late settlement without making telemetry lifecycle-load-bearing. `emitBoundedRunAudit` is the default void seam. `emitBoundedRunAuditWithOutcome` returns `recorded`, `absent`, `failed` (with the original error), or `timed-out` where a forensic throw ordering or caller-visible skipped payload depends on the audit result; workflow-switch torn reconciliation and phantom committed-reservation reconciliation use it. FN-9181 applies FN-9178's class-A decision to detached recall capture: `memory:capture-recorded` and `memory:capture-failed` are bounded, while the injectable `deps.audit` adapter remains a test seam with its existing bare-metadata contract. Transactional writers and explicitly awaited durability/ordering writers remain unbounded. `packages/core/src/__tests__/core-run-audit-sink-health.test.ts` and `core-run-audit-emitter-isolation.test.ts` respectively enforce hostile-sink behavior and source routing.
|
||||
|
||||
### Awaited core exclusion decision
|
||||
|
||||
FN-9178 classified awaited sites with hostile-sink characterization tests. FN-9180 routed the class-A `task-deleted-outbox:catch-up`, `:reconciliation-fallback`, `:lease-fenced`, and `:retention-pruned` rows through `emitBoundedRunAudit`; each remains awaited at its post-acknowledgement, post-cursor, or post-DELETE position so bounded telemetry preserves ordering. Recall capture remains class A and is owned by FN-9181. `task:workflow-switch-torn` and `task:reconcile-phantom-committed-reservation` are class B and use the bounded outcome seam because their throw/result payload depends on audit outcome. `task:bypass-review`, `task:resume-step`, and both resurrection-blocked records are class C and intentionally unbounded: they claim persistence before return, destructive cleanup, or a forensic throw.
|
||||
FN-9178 classified awaited sites with hostile-sink characterization tests. FN-9180 routed the class-A `task-deleted-outbox:catch-up`, `:reconciliation-fallback`, `:lease-fenced`, and `:retention-pruned` rows through `emitBoundedRunAudit`; each remains awaited at its post-acknowledgement, post-cursor, or post-DELETE position so bounded telemetry preserves ordering. FN-9181 routed detached recall capture through the same bounded seam. `task:workflow-switch-torn` and `task:reconcile-phantom-committed-reservation` are class B and use the bounded outcome seam because their throw/result payload depends on audit outcome. `task:bypass-review`, `task:resume-step`, and both resurrection-blocked records are class C and intentionally unbounded: they claim persistence before return, destructive cleanup, or a forensic throw.
|
||||
|
||||
All `recordRunAuditEventWithinTransaction(tx, ...)` calls and the `recordRunAuditEventBackend(tx, ...)` transactional call are permanently out of scope. Their audit row shares a transaction with the mutation it describes; bounding would split that atomicity. The full matrix and evidence pointers are in the FN-9178 `decision` task document; `excluded-awaited-run-audit-store-sites.test.ts`, `excluded-awaited-run-audit-layer-sites.test.ts`, and the core routing ratchet pin this boundary.
|
||||
|
||||
@@ -14,7 +14,6 @@ const awaitedClassifications = {
|
||||
"store.ts:task:resume-step": "C",
|
||||
"task-store/task-creation.ts:intake:resurrection-blocked": "C",
|
||||
"task-store/task-id-integrity.ts:task:resurrection-blocked": "C",
|
||||
"memory/recall-capture.ts:memory:capture-recorded|memory:capture-failed": "A",
|
||||
"task-store/project-store-ops.ts:recordRunAuditEventImpl": "permanent-sink",
|
||||
} as const;
|
||||
|
||||
@@ -32,7 +31,7 @@ const files = [
|
||||
"../task-store/workflow-workitems-ops.ts", "../task-store/workflow-workitems-ops-2.ts", "../task-store/task-artifacts-ops.ts",
|
||||
"../task-store/lifecycle-ops.ts", "../task-store/task-id-integrity.ts", "../task-store/workflow-definitions.ts",
|
||||
"../task-store/async/async-phantom-reservations.ts", "../task-store/task-deleted-outbox-consumer.ts",
|
||||
"../task-store/task-lifecycle-event-retention.ts",
|
||||
"../task-store/task-lifecycle-event-retention.ts", "../memory/recall-capture.ts",
|
||||
];
|
||||
|
||||
const sourceRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { asyncAuditSink, workflowReader, resolvedWorkflowIr } = vi.hoisted(() => ({
|
||||
const { asyncAuditSink, recallAppend, workflowReader, resolvedWorkflowIr } = vi.hoisted(() => ({
|
||||
asyncAuditSink: { current: undefined as undefined | ((event: unknown) => unknown) },
|
||||
recallAppend: { current: undefined as undefined | ((input: unknown) => unknown) },
|
||||
workflowReader: { current: undefined as undefined | (() => unknown) },
|
||||
resolvedWorkflowIr: { current: undefined as unknown },
|
||||
}));
|
||||
@@ -14,6 +15,10 @@ vi.mock("../postgres/data-layer.js", async (importOriginal) => {
|
||||
return { ...actual, recordRunAuditEvent: (_layer: unknown, event: unknown) => asyncAuditSink.current?.(event) };
|
||||
});
|
||||
|
||||
vi.mock("../memory/recall/recall-store.js", () => ({
|
||||
appendRecall: (_layer: unknown, input: unknown) => recallAppend.current?.(input),
|
||||
}));
|
||||
|
||||
vi.mock("../task-store/async/async-persistence.js", () => ({
|
||||
readTaskRow: vi.fn(async () => workflowReader.current?.()),
|
||||
}));
|
||||
@@ -54,6 +59,7 @@ import {
|
||||
emitOverseerRetry,
|
||||
emitOverseerSteering,
|
||||
} from "../planner/planner-overseer-events.js";
|
||||
import { createRecallCaptureWriter } from "../memory/recall-capture.js";
|
||||
import { CORE_RUN_AUDIT_EMIT_TIMEOUT_MS } from "../run-audit/emit-bounded-run-audit.js";
|
||||
import { runPluginColumnTransitionHooksImpl } from "../task-store/audit-ops.js";
|
||||
import { rehomeOccupantImpl } from "../task-store/branch-group-ops.js";
|
||||
@@ -102,9 +108,61 @@ async function settleBounded(promise: Promise<void>, mode: SinkMode, settle: ()
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
}
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
afterEach(() => {
|
||||
asyncAuditSink.current = undefined;
|
||||
recallAppend.current = undefined;
|
||||
workflowReader.current = undefined;
|
||||
resolvedWorkflowIr.current = undefined;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("core audit emitters tolerate hostile sinks", () => {
|
||||
it.each(["absent", "throw", "reject", "never", "late-resolve", "late-reject"] as const)(
|
||||
"keeps production recall-capture audit bounded for %s layer sinks", async (mode) => {
|
||||
vi.useFakeTimers();
|
||||
const hostile = hostileSink(mode);
|
||||
asyncAuditSink.current = mode === "absent" ? undefined : hostile.sink;
|
||||
recallAppend.current = () => ({
|
||||
status: "created",
|
||||
record: { id: "recall-9177", projectId: "project", kind: "decision", content: "stored", contentHash: "hash", source: { origin: "other" }, tags: [], graphNodeIds: [], createdAt: "2026-08-20T00:00:00.000Z", updatedAt: "2026-08-20T00:00:00.000Z" },
|
||||
});
|
||||
const writer = createRecallCaptureWriter({ layer: {} as never, logger: { warn: vi.fn() } });
|
||||
expect(writer.capture({ origin: "insight", summary: "must not reach audit", insightId: "INS-9177" })).toBeUndefined();
|
||||
const drain = writer.flushPendingCaptures();
|
||||
if (mode === "never" || mode.startsWith("late-")) {
|
||||
await vi.advanceTimersByTimeAsync(CORE_RUN_AUDIT_EMIT_TIMEOUT_MS);
|
||||
hostile.settle();
|
||||
}
|
||||
await expect(drain).resolves.toBeUndefined();
|
||||
await expect(writer.flushPendingCaptures()).resolves.toBeUndefined();
|
||||
if (mode !== "absent") expect(hostile.sink).toHaveBeenCalled();
|
||||
if (mode === "late-reject") await Promise.resolve();
|
||||
if (mode === "absent") return;
|
||||
const event = hostile.sink.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(event).toMatchObject({ agentId: "memory-capture", domain: "database", mutationType: "memory:capture-recorded", target: "INS-9177" });
|
||||
expect(event.metadata).toMatchObject({ origin: "insight", recallRecordId: "recall-9177", outcome: "created" });
|
||||
expect(JSON.stringify(event)).not.toContain("must not reach audit");
|
||||
},
|
||||
);
|
||||
|
||||
it("records the production layer failure event and suppresses the layer when append is injected", async () => {
|
||||
const sink = vi.fn();
|
||||
asyncAuditSink.current = sink;
|
||||
recallAppend.current = () => Promise.reject(new TypeError("append failure"));
|
||||
const writer = createRecallCaptureWriter({ layer: {} as never, logger: { warn: vi.fn() } });
|
||||
writer.capture({ origin: "research-finding", summary: "private failure summary", findingId: "RF-9177" });
|
||||
await writer.flushPendingCaptures();
|
||||
expect(sink).toHaveBeenCalledWith(expect.objectContaining({
|
||||
mutationType: "memory:capture-failed", target: "RF-9177", metadata: { origin: "research-finding", errorClass: "TypeError" },
|
||||
}));
|
||||
|
||||
const injectedAppend = vi.fn(async () => ({ status: "created" as const, record: { id: "recall-injected" } }));
|
||||
const injectedWriter = createRecallCaptureWriter({ layer: {} as never, logger: { warn: vi.fn() }, append: injectedAppend as never });
|
||||
injectedWriter.capture({ origin: "insight", summary: "append injection suppresses audit" });
|
||||
await injectedWriter.flushPendingCaptures();
|
||||
expect(sink).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(["absent", "throw", "reject", "never", "late-resolve", "late-reject"] as const)(
|
||||
"keeps every planner façade bounded for %s sinks", async (mode) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CORE_RUN_AUDIT_EMIT_TIMEOUT_MS } from "../../run-audit/emit-bounded-run-audit.js";
|
||||
import {
|
||||
buildRecallCaptureContent,
|
||||
createRecallCaptureWriter,
|
||||
@@ -11,6 +12,31 @@ import type { RecallAppendInput } from "../../memory/recall/recall-types.js";
|
||||
const layer = {} as never;
|
||||
const logger = { warn: vi.fn() };
|
||||
|
||||
type AuditSinkMode = "absent" | "throw" | "reject" | "never" | "late-resolve" | "late-reject";
|
||||
|
||||
function hostileAudit(mode: AuditSinkMode) {
|
||||
let settle: (() => void) | undefined;
|
||||
const audit = vi.fn(() => {
|
||||
if (mode === "throw") throw new Error("audit throw");
|
||||
if (mode === "reject") return Promise.reject(new Error("audit rejection"));
|
||||
if (mode === "never") return new Promise<void>(() => undefined);
|
||||
if (mode === "late-resolve") return new Promise<void>((resolve) => { settle = resolve; });
|
||||
if (mode === "late-reject") return new Promise<void>((_resolve, reject) => { settle = () => reject(new Error("late audit rejection")); });
|
||||
return undefined;
|
||||
});
|
||||
return { audit, settle: () => settle?.() };
|
||||
}
|
||||
|
||||
async function drainBounded(writer: ReturnType<typeof createRecallCaptureWriter>, mode: AuditSinkMode, settle: () => void) {
|
||||
const drain = writer.flushPendingCaptures();
|
||||
if (mode === "never" || mode.startsWith("late-")) {
|
||||
await vi.advanceTimersByTimeAsync(CORE_RUN_AUDIT_EMIT_TIMEOUT_MS);
|
||||
settle();
|
||||
}
|
||||
await expect(drain).resolves.toBeUndefined();
|
||||
await expect(writer.flushPendingCaptures()).resolves.toBeUndefined();
|
||||
}
|
||||
|
||||
function created(input: RecallAppendInput) {
|
||||
return {
|
||||
status: "created" as const,
|
||||
@@ -22,6 +48,8 @@ function created(input: RecallAppendInput) {
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
describe("recall capture writer", () => {
|
||||
it("maps every automatic origin to FN-8922 kinds and source provenance", async () => {
|
||||
const append = vi.fn(async (input: RecallAppendInput) => created(input));
|
||||
@@ -64,6 +92,47 @@ describe("recall capture writer", () => {
|
||||
expect(warn).toHaveBeenCalledWith("Automatic recall capture failed for research-finding");
|
||||
});
|
||||
|
||||
it.each(["absent", "throw", "reject", "never", "late-resolve", "late-reject"] as const)(
|
||||
"keeps injectable capture audit bounded for %s sinks", async (mode) => {
|
||||
vi.useFakeTimers();
|
||||
const hostile = hostileAudit(mode);
|
||||
const append = vi.fn(async (input: RecallAppendInput) => created(input));
|
||||
const warn = vi.fn();
|
||||
const writer = createRecallCaptureWriter({
|
||||
layer,
|
||||
logger: { warn },
|
||||
append,
|
||||
...(mode === "absent" ? {} : { audit: hostile.audit }),
|
||||
});
|
||||
|
||||
expect(writer.capture({ origin: "insight", summary: "secret capture summary", insightId: "INS-1" })).toBeUndefined();
|
||||
await drainBounded(writer, mode, hostile.settle);
|
||||
expect(append).toHaveBeenCalledTimes(1);
|
||||
if (mode === "absent") expect(hostile.audit).not.toHaveBeenCalled();
|
||||
else expect(hostile.audit).toHaveBeenCalledWith({ type: "memory:capture-recorded", metadata: { recallRecordId: "recall-1", outcome: "created" } });
|
||||
expect(JSON.stringify([hostile.audit.mock.calls, warn.mock.calls])).not.toContain("secret capture summary");
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves duplicate and failure adapter metadata without invoking the layer sink", async () => {
|
||||
const audit = vi.fn(async () => {});
|
||||
const duplicate = { ...created({ kind: "decision", content: "x", source: { origin: "other" } }), id: "recall-duplicate" };
|
||||
const append = vi.fn()
|
||||
.mockResolvedValueOnce({ status: "duplicate" as const, duplicateOf: duplicate, similarity: 1 })
|
||||
.mockRejectedValueOnce(new TypeError("append failure"));
|
||||
const warn = vi.fn();
|
||||
const writer = createRecallCaptureWriter({ layer, logger: { warn }, append, audit });
|
||||
|
||||
writer.capture({ origin: "insight", summary: "never expose this summary" });
|
||||
writer.capture({ origin: "research-finding", summary: "nor this failure summary" });
|
||||
await writer.flushPendingCaptures();
|
||||
|
||||
expect(audit).toHaveBeenNthCalledWith(1, { type: "memory:capture-recorded", metadata: { recallRecordId: "recall-duplicate", outcome: "duplicate" } });
|
||||
expect(audit).toHaveBeenNthCalledWith(2, { type: "memory:capture-failed", metadata: { errorClass: "TypeError" } });
|
||||
expect(warn).toHaveBeenCalledWith("Automatic recall capture failed for research-finding");
|
||||
expect(JSON.stringify([audit.mock.calls, warn.mock.calls])).not.toContain("summary");
|
||||
});
|
||||
|
||||
it("records ids-only capture audit metadata after persistence", async () => {
|
||||
const audit = vi.fn(async () => {});
|
||||
const append = vi.fn(async (input: RecallAppendInput) => created(input));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { recordRunAuditEvent, type AsyncDataLayer } from "../postgres/data-layer.js";
|
||||
import type { Logger } from "../process/logger.js";
|
||||
import { emitBoundedRunAudit } from "../run-audit/emit-bounded-run-audit.js";
|
||||
import type { RunAuditEventInput } from "../types.js";
|
||||
import { appendRecall } from "./recall/recall-store.js";
|
||||
import type { RecallAppendInput, RecallAppendResult, RecallKind, RecallOrigin } from "./recall/recall-types.js";
|
||||
|
||||
@@ -77,6 +79,11 @@ FNXC:MemoryRecallCapture 2026-08-11-10:57:
|
||||
FN-8922 has no insight-specific RecallOrigin literal, so insight outcomes use its real "other"
|
||||
origin while completed tasks and research use "task-completion" and "deep-research" respectively.
|
||||
The test drain exists solely for deterministic detached-work tests and production must never call it.
|
||||
|
||||
FNXC:RunAudit 2026-08-20-07:03:
|
||||
FN-9181 implements FN-9178's class-A decision: bounded audit emission prevents a stalled sink from
|
||||
retaining a detached capture promise. Preserve the injectable adapter's bare-metadata contract, and
|
||||
suppress the layer path whenever append is injected so the deterministic test seam remains isolated.
|
||||
*/
|
||||
export const NOOP_RECALL_CAPTURE_WRITER: RecallCaptureWriter = Object.freeze({
|
||||
capture: () => {},
|
||||
@@ -134,10 +141,18 @@ export function createRecallCaptureWriter(
|
||||
const pending = new Set<Promise<void>>();
|
||||
const append = deps.append ?? ((input: RecallAppendInput) => appendRecall(deps.layer, input));
|
||||
const recordAudit = async (type: "memory:capture-recorded" | "memory:capture-failed", input: RecallCaptureInput, metadata: Record<string, string>) => {
|
||||
const log = { warn: () => deps.logger.warn(`Automatic recall capture audit failed for ${input.origin}`) };
|
||||
if (deps.audit) {
|
||||
await deps.audit({ type, metadata });
|
||||
await emitBoundedRunAudit({
|
||||
recordRunAuditEvent: (event) => deps.audit?.({
|
||||
type: event.mutationType as "memory:capture-recorded" | "memory:capture-failed",
|
||||
metadata,
|
||||
}),
|
||||
}, { mutationType: type }, { log });
|
||||
} else if (!deps.append) {
|
||||
await recordRunAuditEvent(deps.layer, {
|
||||
await emitBoundedRunAudit({
|
||||
recordRunAuditEvent: (event) => recordRunAuditEvent(deps.layer, event as RunAuditEventInput),
|
||||
}, {
|
||||
agentId: input.agentId ?? "memory-capture",
|
||||
runId: `memory-capture:${input.origin}:${Date.now()}`,
|
||||
taskId: input.taskId,
|
||||
@@ -145,7 +160,7 @@ export function createRecallCaptureWriter(
|
||||
mutationType: type,
|
||||
target: input.insightId ?? input.findingId ?? input.researchRunId ?? input.taskId ?? input.origin,
|
||||
metadata: { origin: input.origin, ...metadata },
|
||||
});
|
||||
}, { log });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,24 +169,16 @@ export function createRecallCaptureWriter(
|
||||
const operation = (async () => {
|
||||
try {
|
||||
const result = await append(toAppendInput(input));
|
||||
try {
|
||||
await recordAudit("memory:capture-recorded", input, {
|
||||
recallRecordId: result.status === "created" ? result.record.id : result.duplicateOf.id,
|
||||
outcome: result.status,
|
||||
});
|
||||
} catch {
|
||||
deps.logger.warn(`Automatic recall capture audit failed for ${input.origin}`);
|
||||
}
|
||||
await recordAudit("memory:capture-recorded", input, {
|
||||
recallRecordId: result.status === "created" ? result.record.id : result.duplicateOf.id,
|
||||
outcome: result.status,
|
||||
});
|
||||
} catch (error) {
|
||||
// Recall content can be sensitive, so diagnostics identify only the bounded origin.
|
||||
deps.logger.warn(`Automatic recall capture failed for ${input.origin}`);
|
||||
try {
|
||||
await recordAudit("memory:capture-failed", input, {
|
||||
errorClass: error instanceof Error ? error.name : "unknown",
|
||||
});
|
||||
} catch {
|
||||
deps.logger.warn(`Automatic recall capture audit failed for ${input.origin}`);
|
||||
}
|
||||
await recordAudit("memory:capture-failed", input, {
|
||||
errorClass: error instanceof Error ? error.name : "unknown",
|
||||
});
|
||||
}
|
||||
})();
|
||||
pending.add(operation);
|
||||
|
||||
Reference in New Issue
Block a user