diff --git a/backend/src/agent/podman.ts b/backend/src/agent/podman.ts index 902d55a..2b895ee 100644 --- a/backend/src/agent/podman.ts +++ b/backend/src/agent/podman.ts @@ -151,12 +151,15 @@ export class PodMan { // durable suppressed-repeat event (timestamped now, at the repeat) so the // activity stream shows the learning instead of nothing. if (prior?.priorOutcome && !prior.priorOutcome.accepted) { - void recordSuppression( + // Mark handled first — like the alert path below — so we record ONE + // suppressed-repeat per recurrence, not once per frame; it re-arms via + // the resolution sweep in onScreenFrame. Awaited like recordCollision so + // the durable learning proof is reliably written. + this.activeConflicts.add(key); + await recordSuppression( collision, prior.priorOutcome.interventionId, prior.priorOutcome.recordedAt, - ).catch((err) => - console.error(`[memory] suppression record failed: ${(err as Error).message}`), ); } return; // Loop B: policy gate diff --git a/backend/src/graph/live.ts b/backend/src/graph/live.ts index 0b9e424..6bf261b 100644 --- a/backend/src/graph/live.ts +++ b/backend/src/graph/live.ts @@ -463,16 +463,23 @@ export async function materializePodGraph(podId: string): Promise(); for (const s of suppressionDocs) { const sFile = normalizeFile(s.file); if (!isFilePath(sFile)) continue; + const fileKey = sFile.toLowerCase(); + if (seenSuppressedFiles.has(fileKey)) continue; + seenSuppressedFiles.add(fileKey); const sEngs = (s.engineers ?? []).join(' + ') || 'teammates'; pushActivity( activity, { - id: `suppressed:${s.id}`, + id: `suppressed:${fileKey}`, at: s.suppressedAt, kind: 'suppressed', title: `Suppressed — ${shortLabel(sFile)} repeat silenced`, @@ -520,7 +527,11 @@ export async function materializePodGraph(podId: string): Promise let the caller fall back to demo. - const hasActivity = nodes.some((n) => n.kind !== 'engineer'); + // Suppression beats are real negative-feedback proof even when they add no + // nodes/edges, so they satisfy the gate too — a clean pod with preserved + // suppressions must not fall back to the demo graph and hide the proof. + const hasSuppressed = activity.some((a) => a.kind === 'suppressed'); + const hasActivity = hasSuppressed || nodes.some((n) => n.kind !== 'engineer'); if (!hasActivity) return null; layout(nodes); diff --git a/backend/src/memory/store.ts b/backend/src/memory/store.ts index 805879f..2325725 100644 --- a/backend/src/memory/store.ts +++ b/backend/src/memory/store.ts @@ -163,11 +163,12 @@ export async function recordOutcome(outcome: InterventionOutcome): Promise /** Document counts per collection — used by the /api/memory/stats endpoint. */ export async function memoryStats(): Promise> { const c = await collections(); - const [observations, collisions, interventions, outcomes] = await Promise.all([ + const [observations, collisions, interventions, outcomes, suppressions] = await Promise.all([ c.observations.estimatedDocumentCount(), c.collisions.estimatedDocumentCount(), c.interventions.estimatedDocumentCount(), c.outcomes.estimatedDocumentCount(), + c.suppressions.estimatedDocumentCount(), ]); - return { observations, collisions, interventions, outcomes }; + return { observations, collisions, interventions, outcomes, suppressions }; } diff --git a/docs/mongodb.md b/docs/mongodb.md index 05c1080..20a74bc 100644 --- a/docs/mongodb.md +++ b/docs/mongodb.md @@ -121,6 +121,28 @@ Key fields: Primary use: accepted and dismissed outcomes drive exact recall, suppression, and learned graph paths. +### `suppressions` + +Durable negative-feedback proof: one record per *suppressed repeat* — a +previously-dismissed collision signature recurred and PodMan stayed quiet. +Written at repeat time by `backend/src/agent/podman.ts` (once per recurrence, +re-armed on resolution), materialized as `suppressed` activity by +`backend/src/graph/live.ts`. + +Key fields: + +- `id` +- `podId` +- `collisionId` +- `file` +- `engineers` +- `priorInterventionId` — the dismissed intervention this repeat matched +- `priorDismissedAt` +- `suppressedAt` — the repeat time (drives recency in the activity stream) + +Index `{ podId: 1, suppressedAt: -1 }`. Counted in `/api/memory/stats`. +**Preserve in any DB cleanup** — this is visible learning evidence, not noise. + ### `team_model` Durable per-pod summary memory.