aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
143 lines • 4.77 kB
JavaScript
/**
* Telemetry Event Schema and In-Memory Store
*
* Defines the canonical event types emitted by the agent loop, MC conductor,
* and ralph launcher. Events are stored in-memory per session and forwarded
* to the browser via WebSocket push. Persisted to fortemi-react IndexedDB
* on the browser side (#717).
*
* Emission points:
* - src/cli/handlers/ralph-launcher.ts → iteration.*, gate.*, tokens.used
* - src/cli/handlers/mc.ts → session.*, mission.*, agent.*
*
* @issue #716
* @see #717 — fortemi-react storage layer (browser)
*/
// ============================================================
// In-Memory Event Store
// ============================================================
const MAX_EVENTS_PER_SESSION = 10_000;
export class TelemetryStore {
events = new Map();
listeners = new Set();
/** Ingest a new event */
ingest(event) {
const list = this.events.get(event.sessionId) ?? [];
list.push(event);
// Trim oldest events when exceeding limit
if (list.length > MAX_EVENTS_PER_SESSION) {
list.splice(0, list.length - MAX_EVENTS_PER_SESSION);
}
this.events.set(event.sessionId, list);
for (const listener of this.listeners) {
try {
listener(event);
}
catch { /* ignore listener errors */ }
}
}
/** Get all events for a session, optionally filtered by type */
query(sessionId, opts = {}) {
let events = this.events.get(sessionId) ?? [];
if (opts.types?.length) {
events = events.filter((e) => opts.types.includes(e.type));
}
if (opts.since) {
events = events.filter((e) => e.timestamp >= opts.since);
}
if (opts.limit) {
events = events.slice(-opts.limit);
}
return events;
}
/** Get aggregate metrics for a session */
metrics(sessionId) {
const events = this.events.get(sessionId) ?? [];
let totalInput = 0;
let totalOutput = 0;
const tokensByModel = {};
let iterations = 0;
let gatePasses = 0;
let gateFails = 0;
let scopeDone = 0;
let scopeTotal = 0;
let activeAgents = 0;
for (const e of events) {
if (e.type === 'tokens.used') {
const p = e.payload;
totalInput += p.input;
totalOutput += p.output;
const m = tokensByModel[p.model] ?? { input: 0, output: 0 };
m.input += p.input;
m.output += p.output;
tokensByModel[p.model] = m;
}
else if (e.type === 'iteration.complete') {
iterations++;
}
else if (e.type === 'gate.pass') {
gatePasses++;
}
else if (e.type === 'gate.fail') {
gateFails++;
}
else if (e.type === 'scope.unit.complete') {
const p = e.payload;
scopeDone = p.done;
scopeTotal = p.total;
}
else if (e.type === 'agent.spawn') {
activeAgents++;
}
else if (e.type === 'agent.complete') {
activeAgents = Math.max(0, activeAgents - 1);
}
}
const passRate = gatePasses + gateFails > 0
? Math.round((gatePasses / (gatePasses + gateFails)) * 100)
: null;
return {
sessionId,
totalInputTokens: totalInput,
totalOutputTokens: totalOutput,
tokensByModel,
iterations,
gatePasses,
gateFails,
passRate,
scopeDone,
scopeTotal,
activeAgents,
};
}
/** Subscribe to new events (returns unsubscribe fn) */
subscribe(listener) {
this.listeners.add(listener);
return () => { this.listeners.delete(listener); };
}
/** Clear all events for a session */
clear(sessionId) {
this.events.delete(sessionId);
}
/** Get all known session IDs */
sessions() {
return [...this.events.keys()];
}
}
// Singleton store
export const telemetryStore = new TelemetryStore();
// ============================================================
// Event Factory
// ============================================================
let eventCounter = 0;
export function createEvent(type, sessionId, payload, missionId) {
return {
id: `evt-${Date.now()}-${++eventCounter}`,
sessionId,
missionId,
timestamp: new Date().toISOString(),
type,
payload,
};
}
//# sourceMappingURL=telemetry.js.map