feat(graph): record + surface suppressed-repeat activity at repeat time (Feature A) (#43)

Codex review fix. The earlier approach synthesized a 'suppressed' beat from every
dismissed OUTCOME, which is wrong: a dismissal is not a later suppressed repeat,
and the old dismissal timestamp sank below the 12-row activity cap (invisible).

Now the negative-feedback loop is recorded WHEN IT HAPPENS. When shouldIntervene()
returns false specifically because a signature was dismissed before
(agent/podman.ts), the agent writes a durable SuppressionDoc to a new
`suppressions` collection, timestamped at the repeat. graph/live.ts materializes
those into kind:'suppressed' activity (recent -> surfaces at the top). The
suppressed collision is never written to `collisions` (agent returns before
recordCollision), so this is its own record.

Verified end-to-end: a transient demo-pod suppression renders at the TOP of the
activity stream; cleaned up after. shared build + backend/frontend typecheck +
eslint pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb-iam
2026-06-28 09:35:56 -07:00
committed by GitHub
parent b6447bb12c
commit 65764f0a1c
7 changed files with 89 additions and 1 deletions
+17 -1
View File
@@ -8,6 +8,7 @@ import {
recordObservation,
recordCollision,
recordIntervention,
recordSuppression,
updateInterventionStatus,
} from '../memory/store.js';
import { getGitStates, type GitState } from '../memory/db.js';
@@ -144,7 +145,22 @@ export class PodMan {
if (prior?.priorOutcome?.accepted && prior?.priorOutcome?.wasRealCollision) {
collision.severity = 'critical';
}
if (!shouldIntervene(collision, prior)) return; // Loop B: policy gate
if (!shouldIntervene(collision, prior)) {
// Feature A — make the negative-feedback loop VISIBLE. If we stayed quiet
// *specifically* because this signature was DISMISSED before, record a
// 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(
collision,
prior.priorOutcome.interventionId,
prior.priorOutcome.recordedAt,
).catch((err) =>
console.error(`[memory] suppression record failed: ${(err as Error).message}`),
);
}
return; // Loop B: policy gate
}
this.activeConflicts.add(key); // claim now we're alerting; re-armed in onScreenFrame on resolution
await recordCollision(collision);
+25
View File
@@ -457,6 +457,31 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
);
}
// 7. Suppressed repeats: PodMan stayed quiet on a recurring collision because
// the signature was dismissed before — the negative-feedback loop made visible
// (Feature A). Stamped at repeat time, so it sorts as recent activity.
const suppressionDocs = await c.suppressions
.find({ podId })
.sort({ suppressedAt: -1 })
.limit(20)
.toArray();
for (const s of suppressionDocs) {
const sFile = normalizeFile(s.file);
if (!isFilePath(sFile)) continue;
const sEngs = (s.engineers ?? []).join(' + ') || 'teammates';
pushActivity(
activity,
{
id: `suppressed:${s.id}`,
at: s.suppressedAt,
kind: 'suppressed',
title: `Suppressed — ${shortLabel(sFile)} repeat silenced`,
detail: `${sEngs} on ${sFile} recurred, but it was dismissed before — PodMan stayed quiet.`,
},
activityIds,
);
}
// Prune test-artifact engineers, then anything left orphaned by that.
const dropNode = (id: string) => {
b.nodes.delete(id);
+19
View File
@@ -31,12 +31,29 @@ export async function closeMemory(): Promise<void> {
await client.close();
}
/** A durable record that PodMan stayed quiet on a recurring collision because
* its signature was previously dismissed — the negative-feedback loop made
* auditable. Timestamped at the repeat; materialized as `suppressed` activity
* in graph/live.ts. The suppressed collision is never written to `collisions`
* (the agent returns before recordCollision), so this is its own record. */
export interface SuppressionDoc {
id: string;
podId: string;
collisionId: string;
file: string;
engineers: string[];
priorInterventionId?: string;
priorDismissedAt?: string;
suppressedAt: string;
}
export interface PodCollections {
pods: Collection<Pod>;
observations: Collection<EngineerContext>;
collisions: Collection<Collision>;
interventions: Collection<Intervention>;
outcomes: Collection<InterventionOutcome>;
suppressions: Collection<SuppressionDoc>;
}
export async function collections(): Promise<PodCollections> {
@@ -47,6 +64,7 @@ export async function collections(): Promise<PodCollections> {
collisions: db.collection<Collision>('collisions'),
interventions: db.collection<Intervention>('interventions'),
outcomes: db.collection<InterventionOutcome>('outcomes'),
suppressions: db.collection<SuppressionDoc>('suppressions'),
};
}
@@ -114,6 +132,7 @@ export async function initMemory(): Promise<void> {
['collisions.file', () => c.collisions.createIndex({ podId: 1, file: 1, detectedAt: -1 })],
['interventions.collisionId', () => c.interventions.createIndex({ collisionId: 1 })],
['outcomes.interventionId', () => c.outcomes.createIndex({ interventionId: 1 })],
['suppressions.podId', () => c.suppressions.createIndex({ podId: 1, suppressedAt: -1 })],
['hermes_jobs.id', () => db.collection('hermes_jobs').createIndex({ id: 1 }, { unique: true })],
[
'hermes_jobs.session',
+25
View File
@@ -50,6 +50,31 @@ export async function recordIntervention(intervention: Intervention): Promise<vo
);
}
/**
* Feature A — record that PodMan stayed quiet on a recurring collision because
* its signature was previously dismissed. Timestamped at the repeat (now), so
* the negative-feedback beat surfaces as recent `suppressed` activity rather
* than being re-synthesized from the old dismissal row.
*/
export async function recordSuppression(
collision: Collision,
priorInterventionId?: string,
priorDismissedAt?: string,
): Promise<void> {
await persist('suppression', async () =>
(await collections()).suppressions.insertOne({
id: `supp_${Date.now()}`,
podId: collision.podId,
collisionId: collision.id,
file: collision.file,
engineers: collision.engineers,
priorInterventionId,
priorDismissedAt,
suppressedAt: new Date().toISOString(),
}),
);
}
export async function hasRecentInterventionForCollision(
collision: Collision,
windowMs = Number(process.env.NUDGE_COOLDOWN_MS ?? '180000'),