a853ef7709
Add getGitStates(podId) to db.ts — queries the engineer_states collection written by scripts/podman-agent.mjs and returns a map keyed by engineer name. In PodMan.onScreenFrame(), fuse before detectCollisions(): if an engineer has changedFiles in engineer_states, force hasUnpushedChanges=true on their context. This overrides Gemini vision's unreliable pixel-based guess with deterministic local git state — preventing the money-moment collision from silently not firing. Key assumption: LiveKit participant identity matches the --name arg used when running the git watcher (e.g. identity "alice" → --name alice). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaCFWMkYQmTcuPsxaaACft
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { RoomEvent, type Room } from '@livekit/rtc-node';
|
|
import type { EngineerContext, Collision, Intervention, DataMessage } from '@podman/shared';
|
|
import { DATA_TOPIC } from '@podman/shared';
|
|
import { analyzeFrame } from '../vision/gemini.js';
|
|
import { detectCollisions } from '../collision/detector.js';
|
|
import { getGithubState } from '../github/client.js';
|
|
import { recordObservation, recordCollision, recordIntervention } from '../memory/store.js';
|
|
import { getGitStates } from '../memory/db.js';
|
|
import { recallSimilar } from '../memory/vectors.js';
|
|
import { shouldIntervene, preferredAction } from '../memory/policy.js';
|
|
import { speak } from '../voice/live.js';
|
|
|
|
export class PodMan {
|
|
private contexts = new Map<string, EngineerContext>();
|
|
private encoder = new TextEncoder();
|
|
|
|
constructor(
|
|
private room: Room,
|
|
private podId: string,
|
|
) {}
|
|
|
|
async start(): Promise<void> {
|
|
// Tier-2 optional ground-truth + engineer ACKs arrive over the data channel.
|
|
this.room.on(RoomEvent.DataReceived, (payload) => {
|
|
try {
|
|
const msg = JSON.parse(new TextDecoder().decode(payload)) as DataMessage;
|
|
if (msg.type === 'GIT_REPORT') {
|
|
const c = this.contexts.get(msg.report.engineerId);
|
|
if (c) c.hasUnpushedChanges = msg.report.unpushedCount > 0 || msg.report.dirtyFiles.length > 0;
|
|
}
|
|
} catch { /* ignore malformed */ }
|
|
});
|
|
}
|
|
|
|
async onScreenFrame(engineerId: string, jpeg: Buffer): Promise<void> {
|
|
const ctx = await analyzeFrame(engineerId, this.podId, jpeg);
|
|
this.contexts.set(engineerId, ctx);
|
|
await recordObservation(ctx);
|
|
|
|
// Fuse git ground truth: engineer_states written by scripts/podman-agent.mjs.
|
|
// Keyed by name (matches --name arg), same as LiveKit participant identity.
|
|
const gitStates = await getGitStates(this.podId);
|
|
for (const [id, c] of this.contexts) {
|
|
const git = gitStates.get(id);
|
|
if (git && git.changedFiles.length > 0) c.hasUnpushedChanges = true;
|
|
}
|
|
|
|
const github = await getGithubState(); // cached
|
|
const collisions = detectCollisions([...this.contexts.values()], github);
|
|
for (const collision of collisions) await this.handle(collision);
|
|
}
|
|
|
|
private async handle(collision: Collision): Promise<void> {
|
|
const prior = await recallSimilar(collision); // Loop A: vector recall raises confidence
|
|
if (prior) collision.severity = 'critical';
|
|
if (!shouldIntervene(collision, prior)) return; // Loop B: policy gate
|
|
|
|
await recordCollision(collision);
|
|
const action = preferredAction(collision, prior);
|
|
const names = collision.engineers.join(' and ');
|
|
const message = `${names} are both editing ${collision.file}` +
|
|
(collision.githubState?.unpushed ? ' and one has unpushed changes.' : '.') +
|
|
(prior ? ` I've seen this conflict pattern before.` : '');
|
|
|
|
const intervention: Intervention = {
|
|
id: `int_${Date.now()}`,
|
|
collisionId: collision.id,
|
|
podId: this.podId,
|
|
kind: 'card',
|
|
message,
|
|
suggestedAction: { kind: action },
|
|
status: 'pending',
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
await recordIntervention(intervention);
|
|
|
|
const data: DataMessage = { type: 'COLLISION', collision, intervention };
|
|
await this.room.localParticipant?.publishData(
|
|
this.encoder.encode(JSON.stringify(data)),
|
|
{ reliable: true, topic: DATA_TOPIC },
|
|
);
|
|
await speak(this.room, message); // gemini-3.1-flash-live voice into the room
|
|
}
|
|
}
|