fix(voice): throttle repeated pod alerts
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user