fix(voice): throttle repeated pod alerts

This commit is contained in:
Yahya Alhinai
2026-06-28 06:49:04 +00:00
parent c1ac687dcf
commit aaf1dc5ede
6 changed files with 61 additions and 4 deletions
+2
View File
@@ -7,6 +7,7 @@ import {
recordObservation, recordObservation,
recordCollision, recordCollision,
recordIntervention, recordIntervention,
hasRecentInterventionForCollision,
updateInterventionStatus, updateInterventionStatus,
} from '../memory/store.js'; } from '../memory/store.js';
import { getGitStates } from '../memory/db.js'; import { getGitStates } from '../memory/db.js';
@@ -66,6 +67,7 @@ export class PodMan {
const prior = await recallSimilar(collision); // Loop A: exact/vector recall raises confidence const prior = await recallSimilar(collision); // Loop A: exact/vector recall raises confidence
if (prior) collision.severity = 'critical'; if (prior) collision.severity = 'critical';
if (!shouldIntervene(collision, prior)) return; // Loop B: policy gate if (!shouldIntervene(collision, prior)) return; // Loop B: policy gate
if (await hasRecentInterventionForCollision(collision)) return;
await recordCollision(collision); await recordCollision(collision);
const action = preferredAction(collision, prior); const action = preferredAction(collision, prior);
+12
View File
@@ -57,6 +57,8 @@ export interface GitState {
gitUpdatedAt: Date | null; gitUpdatedAt: Date | null;
} }
const GIT_STATE_TTL_MS = Number(process.env.GIT_STATE_TTL_MS ?? '120000');
/** Fetch latest git state per engineer for a pod from the engineer_states collection. /** Fetch latest git state per engineer for a pod from the engineer_states collection.
* Returns a map keyed by engineer name (matches --name arg used in podman-agent.mjs). */ * Returns a map keyed by engineer name (matches --name arg used in podman-agent.mjs). */
export async function getGitStates(podId: string): Promise<Map<string, GitState>> { export async function getGitStates(podId: string): Promise<Map<string, GitState>> {
@@ -71,7 +73,17 @@ export async function getGitStates(podId: string): Promise<Map<string, GitState>
}>('engineer_states'); }>('engineer_states');
const docs = await col.find({ podId }).toArray(); const docs = await col.find({ podId }).toArray();
const map = new Map<string, GitState>(); const map = new Map<string, GitState>();
const now = Date.now();
for (const doc of docs) { for (const doc of docs) {
const updatedAt = doc.gitUpdatedAt ? new Date(doc.gitUpdatedAt) : null;
if (
updatedAt &&
!Number.isNaN(updatedAt.getTime()) &&
GIT_STATE_TTL_MS > 0 &&
now - updatedAt.getTime() > GIT_STATE_TTL_MS
) {
continue;
}
map.set(doc.name, { map.set(doc.name, {
changedFiles: doc.changedFiles ?? [], changedFiles: doc.changedFiles ?? [],
branch: doc.branch ?? null, branch: doc.branch ?? null,
+1 -1
View File
@@ -16,7 +16,7 @@ export function shouldIntervene(collision: Collision, prior: RecalledCollision |
const cooldown = cooldownMs(); const cooldown = cooldownMs();
const last = lastNudgeByPod.get(collision.podId) ?? 0; const last = lastNudgeByPod.get(collision.podId) ?? 0;
if (cooldown > 0 && Date.now() - last < cooldown && collision.severity !== 'critical') { if (cooldown > 0 && Date.now() - last < cooldown) {
return false; return false;
} }
+34
View File
@@ -8,6 +8,15 @@ import type {
import { collections } from './db.js'; import { collections } from './db.js';
import { enrichCollisionMemory } from './vectors.js'; import { enrichCollisionMemory } from './vectors.js';
function comparableFile(raw?: string): string {
return (raw ?? '')
.trim()
.replace(/^(\?\?|[MADRCU!]{1,2})\s+/, '')
.split(/[\\/]/)
.pop()
?.toLowerCase() ?? '';
}
/** /**
* Continual-learning memory: persist observations, collisions, interventions, * Continual-learning memory: persist observations, collisions, interventions,
* and outcomes to MongoDB so later sessions get sharper. MongoDB is mandatory — * and outcomes to MongoDB so later sessions get sharper. MongoDB is mandatory —
@@ -41,6 +50,31 @@ export async function recordIntervention(intervention: Intervention): Promise<vo
); );
} }
export async function hasRecentInterventionForCollision(
collision: Collision,
windowMs = Number(process.env.NUDGE_COOLDOWN_MS ?? '180000'),
): Promise<boolean> {
if (windowMs <= 0) return false;
const c = await collections();
const since = new Date(Date.now() - windowMs).toISOString();
const recent = await c.collisions
.find({ podId: collision.podId, detectedAt: { $gte: since } })
.sort({ detectedAt: -1 })
.limit(100)
.toArray();
const targetFile = comparableFile(collision.file);
for (const match of recent) {
if (match.id === collision.id || comparableFile(match.file) !== targetFile) continue;
const existing = await c.interventions.findOne({
collisionId: match.id,
createdAt: { $gte: since },
});
if (existing) return true;
}
return false;
}
export async function updateInterventionStatus( export async function updateInterventionStatus(
interventionId: string, interventionId: string,
status: InterventionStatus, status: InterventionStatus,
+10 -1
View File
@@ -6,7 +6,13 @@ import { WebSocketServer } from 'ws';
import { AccessToken, RoomAgentDispatch, RoomConfiguration } from 'livekit-server-sdk'; import { AccessToken, RoomAgentDispatch, RoomConfiguration } from 'livekit-server-sdk';
import { env } from './env.js'; import { env } from './env.js';
import { createSyncPr } from './github/client.js'; import { createSyncPr } from './github/client.js';
import { recordCollision, recordIntervention, recordOutcome, memoryStats } from './memory/store.js'; import {
recordCollision,
recordIntervention,
recordOutcome,
hasRecentInterventionForCollision,
memoryStats,
} from './memory/store.js';
import { closeMemory, initMemory } from './memory/db.js'; import { closeMemory, initMemory } from './memory/db.js';
import { import {
listPods, listPods,
@@ -231,6 +237,9 @@ app.post('/api/pods/:id/hermes/notify', async (req, res) => {
: undefined; : undefined;
try { try {
if (body.force !== true && (await hasRecentInterventionForCollision(collision))) {
return res.status(202).json({ ok: true, collision, intervention, livekit: 'suppressed' });
}
await recordCollision(collision); await recordCollision(collision);
await recordIntervention(intervention); await recordIntervention(intervention);
if (body.dryRun === true) { if (body.dryRun === true) {
+2 -2
View File
@@ -544,8 +544,8 @@ export function PodView({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<LiveWaveform <LiveWaveform
processing={!!room} processing={!!room || beat.on}
active={beat.on} active={false}
height={32} height={32}
barWidth={2} barWidth={2}
barGap={3} barGap={3}