feat(commands,audit): add /context command and proactive compaction audit events

This commit is contained in:
William Valentin
2026-02-16 16:08:21 -08:00
parent 9c8da41610
commit 21d57d991c
11 changed files with 173 additions and 20 deletions
+37 -14
View File
@@ -927,6 +927,10 @@ describe('AgentOrchestrator', () => {
deleteConfig: vi.fn(),
};
const sessionCheckpoint = vi.fn();
const previousAuditLogger = auditLogger;
initAuditLogger({ sessionCheckpoint } as unknown as AuditLogger);
const orchestrator = new AgentOrchestrator({
modelRouter: mockRouter,
systemPrompt: 'You are helpful.',
@@ -958,17 +962,23 @@ describe('AgentOrchestrator', () => {
},
});
await orchestrator.process('ping');
const alert = orchestrator.consumeContextAlert();
expect(alert?.level).toBe('checkpoint');
expect(alert?.actions.checkpointSaved).toBe(true);
expect(alert?.actions.checkpointNamespace).toContain('session/checkpoints');
expect(orchestrator.consumeContextAlert()).toBeUndefined();
try {
await orchestrator.process('ping');
const alert = orchestrator.consumeContextAlert();
expect(alert?.level).toBe('checkpoint');
expect(alert?.actions.checkpointSaved).toBe(true);
expect(alert?.actions.checkpointNamespace).toContain('session/checkpoints');
expect(orchestrator.consumeContextAlert()).toBeUndefined();
const stored = memoryStore.read('session/checkpoints/ws/context-test');
expect(stored.length).toBeGreaterThan(0);
rmSync(tempDir, { recursive: true, force: true });
const stored = memoryStore.read('session/checkpoints/ws/context-test');
expect(stored.length).toBeGreaterThan(0);
expect(sessionCheckpoint).toHaveBeenCalledWith(expect.objectContaining({
session_id: 'ws:context-test',
}));
} finally {
initAuditLogger(previousAuditLogger as unknown as AuditLogger);
rmSync(tempDir, { recursive: true, force: true });
}
});
it('auto-compacts proactively at critical threshold and emits alert', async () => {
@@ -1006,6 +1016,11 @@ describe('AgentOrchestrator', () => {
deleteConfig: vi.fn(),
};
const sessionAutoCompact = vi.fn();
const sessionCompact = vi.fn();
const previousAuditLogger = auditLogger;
initAuditLogger({ sessionAutoCompact, sessionCompact } as unknown as AuditLogger);
const orchestrator = new AgentOrchestrator({
modelRouter: compactRouter,
systemPrompt: 'You are helpful.',
@@ -1036,10 +1051,18 @@ describe('AgentOrchestrator', () => {
},
});
await orchestrator.process('continue');
const alert = orchestrator.consumeContextAlert();
expect(alert?.actions.autoCompacted).toBe(true);
expect(history.length).toBeLessThan(6);
try {
await orchestrator.process('continue');
const alert = orchestrator.consumeContextAlert();
expect(alert?.actions.autoCompacted).toBe(true);
expect(history.length).toBeLessThan(6);
expect(sessionAutoCompact).toHaveBeenCalledWith(expect.objectContaining({
session_id: 'ws:auto-compact',
compacted: true,
}));
} finally {
initAuditLogger(previousAuditLogger as unknown as AuditLogger);
}
});
});
});
+17
View File
@@ -573,6 +573,7 @@ export class AgentOrchestrator {
}
if (level === 'critical') {
const beforeAuto = budget;
try {
const result = await this.compact();
autoCompacted = Boolean(result && result.compactedCount > 0);
@@ -580,6 +581,16 @@ export class AgentOrchestrator {
console.warn('[Flynn:compact] Proactive auto-compaction failed:', error);
}
budget = this.getContextBudget();
if (this._session) {
auditLogger?.sessionAutoCompact({
session_id: this._session.id,
usage_pct_before: beforeAuto.usagePct,
usage_pct_after: budget.usagePct,
compacted: autoCompacted,
tokens_before: beforeAuto.estimatedTokens,
tokens_after: budget.estimatedTokens,
});
}
level = this._resolveContextAlertLevel(budget.usagePct);
}
@@ -691,6 +702,12 @@ export class AgentOrchestrator {
const namespace = `${namespaceBase}/${this._sanitizeSessionId(this._session.id)}`;
const block = `## ${new Date().toISOString()}\n\n${summary}\n\n`;
this._memoryStore.write(namespace, block, 'append');
auditLogger?.sessionCheckpoint({
session_id: this._session.id,
namespace,
chars_written: block.length,
usage_pct: this.getContextBudget().usagePct,
});
this._lastCheckpointAt = Date.now();
return { saved: true, namespace };
} catch (error) {