fix(audit): resolve lint global, compaction metrics, and nudge id

This commit is contained in:
William Valentin
2026-02-15 21:54:12 -08:00
parent 50dcff5ea6
commit 948d589ac3
7 changed files with 106 additions and 11 deletions
@@ -9,6 +9,9 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor
- ✅ F-006 addressed: inbound HTTP request bodies now enforce a configurable max-size limit (`server.max_request_body_bytes`) with `413 Payload Too Large` responses. - ✅ F-006 addressed: inbound HTTP request bodies now enforce a configurable max-size limit (`server.max_request_body_bytes`) with `413 Payload Too Large` responses.
- ✅ F-007 addressed: `ToolExecutor` timeout timer handles are now cleared in `finally`, preventing orphan timers on fast/failed tool calls. - ✅ F-007 addressed: `ToolExecutor` timeout timer handles are now cleared in `finally`, preventing orphan timers on fast/failed tool calls.
- ✅ F-016 partially addressed: gateway + webhook body readers were consolidated into shared utility `src/utils/httpBody.ts` with size-limit enforcement. - ✅ F-016 partially addressed: gateway + webhook body readers were consolidated into shared utility `src/utils/httpBody.ts` with size-limit enforcement.
- ✅ F-005 addressed: ESLint JS globals now include `FileReader`, removing UI false-positive lint failures for attachment handling code.
- ✅ F-010 addressed: `session.compact` audit events now emit actual message counts for `messages_before/messages_after` (tokens remain in token fields).
- ✅ F-012 addressed: synthetic repeated-tool nudge no longer emits invalid `tool_result.tool_use_id`; nudge is injected as plain user text guidance.
## Executive Summary ## Executive Summary
+15
View File
@@ -2463,6 +2463,21 @@
"docs/deployment/PRODUCTION.md" "docs/deployment/PRODUCTION.md"
], ],
"test_status": "targeted: pnpm test:run src/gateway/server.test.ts src/automation/webhooks.test.ts src/tools/executor.test.ts src/config/schema.test.ts src/gateway/ui/lib/markdown.test.ts src/utils/httpBody.test.ts + pnpm typecheck" "test_status": "targeted: pnpm test:run src/gateway/server.test.ts src/automation/webhooks.test.ts src/tools/executor.test.ts src/config/schema.test.ts src/gateway/ui/lib/markdown.test.ts src/utils/httpBody.test.ts + pnpm typecheck"
},
"audit-followup-lint-compaction-nudge": {
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Second audit remediation batch: fixed ESLint browser globals mismatch for FileReader, corrected compaction audit event message-count fields, and replaced invalid synthetic tool_result nudge IDs with plain-text nudge guidance in NativeAgent tool loop.",
"files_modified": [
"eslint.config.js",
"src/backends/native/orchestrator.ts",
"src/backends/native/orchestrator.test.ts",
"src/backends/native/agent.ts",
"src/backends/native/agent.test.ts",
"docs/plans/analysis/2026-02-16-codebase-audit-report.md"
],
"test_status": "pnpm test:run src/backends/native/agent.test.ts src/backends/native/orchestrator.test.ts + pnpm typecheck passing"
} }
}, },
"overall_progress": { "overall_progress": {
+1
View File
@@ -116,6 +116,7 @@ export default [
WebSocket: 'readonly', WebSocket: 'readonly',
location: 'readonly', location: 'readonly',
URLSearchParams: 'readonly', URLSearchParams: 'readonly',
FileReader: 'readonly',
navigator: 'readonly', navigator: 'readonly',
alert: 'readonly', alert: 'readonly',
confirm: 'readonly', confirm: 'readonly',
+2 -3
View File
@@ -201,9 +201,8 @@ describe('NativeAgent tool loop', () => {
callCount++; callCount++;
// After nudge message, model should respond with text // After nudge message, model should respond with text
const lastMsg = req.messages[req.messages.length - 1]; const lastMsg = req.messages[req.messages.length - 1];
const hasNudge = typeof lastMsg?.content !== 'string' && const hasNudge = typeof lastMsg?.content === 'string'
Array.isArray(lastMsg?.content) && && lastMsg.content.includes('do NOT call it again');
lastMsg.content.some((b: any) => b.content?.includes('do NOT call it again'));
if (hasNudge) { if (hasNudge) {
return { return {
content: 'Here is what I found from my searches.', content: 'Here is what I found from my searches.',
+5 -6
View File
@@ -339,18 +339,17 @@ export class NativeAgent {
// If the same tool has been called too many times, append a nudge // If the same tool has been called too many times, append a nudge
// telling the model to use what it has. This combats local models // telling the model to use what it has. This combats local models
// that endlessly retry searches with slight query variations. // that endlessly retry searches with slight query variations.
let nudgeMessage: string | null = null;
if (sameToolStreak >= maxSameToolStreak && !nudged) { if (sameToolStreak >= maxSameToolStreak && !nudged) {
nudged = true; nudged = true;
toolResultBlocks.push({ nudgeMessage = `You have called this tool ${sameToolStreak} times in a row. You have enough information — do NOT call it again. Summarize what you have found and respond to the user now.`;
type: 'tool_result',
tool_use_id: '__system',
content: `You have called this tool ${sameToolStreak} times in a row. You have enough information — do NOT call it again. Summarize what you have found and respond to the user now.`,
is_error: false,
});
} }
// Add tool results as a user message // Add tool results as a user message
loopMessages.push({ role: 'user', content: toolResultBlocks }); loopMessages.push({ role: 'user', content: toolResultBlocks });
if (nudgeMessage) {
loopMessages.push({ role: 'user', content: nudgeMessage });
}
// Break out if the model is stuck in a repeated tool call loop // Break out if the model is stuck in a repeated tool call loop
if (consecutiveRepeats >= maxConsecutiveRepeats) { if (consecutiveRepeats >= maxConsecutiveRepeats) {
+78
View File
@@ -8,6 +8,8 @@ import { MemoryStore } from '../../memory/store.js';
import { mkdtempSync, rmSync } from 'fs'; import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { join } from 'path'; import { join } from 'path';
import { auditLogger, initAuditLogger } from '../../audit/index.js';
import type { AuditLogger } from '../../audit/index.js';
describe('AgentOrchestrator', () => { describe('AgentOrchestrator', () => {
let mockDefaultClient: ModelClient; let mockDefaultClient: ModelClient;
@@ -450,6 +452,82 @@ describe('AgentOrchestrator', () => {
}); });
}); });
describe('compact()', () => {
it('emits compaction audit event with message counts (not token counts)', async () => {
const compactClient: ModelClient = {
chat: vi.fn().mockResolvedValue({
content: 'summary',
stopReason: 'end_turn',
usage: { inputTokens: 8, outputTokens: 4 },
}),
};
const compactRouter = new ModelRouter({
default: compactClient,
fast: compactClient,
complex: compactClient,
fallbackChain: [],
});
const history: any[] = [
{ role: 'user', content: 'u1' },
{ role: 'assistant', content: 'a1' },
{ role: 'user', content: 'u2' },
{ role: 'assistant', content: 'a2' },
{ role: 'user', content: 'u3' },
{ role: 'assistant', content: 'a3' },
];
const session = {
id: 'session-compact-audit',
addMessage: vi.fn((m: any) => { history.push(m); }),
getHistory: vi.fn(() => [...history]),
clear: vi.fn(() => { history.length = 0; }),
replaceHistory: vi.fn((msgs: any[]) => {
history.length = 0;
history.push(...msgs);
}),
getConfig: vi.fn(() => undefined),
setConfig: vi.fn(),
deleteConfig: vi.fn(),
} as any;
const sessionCompact = vi.fn();
const previousAuditLogger = auditLogger;
initAuditLogger({ sessionCompact } as unknown as AuditLogger);
try {
const orchestrator = new AgentOrchestrator({
modelRouter: compactRouter,
systemPrompt: 'You are helpful.',
session,
primaryTier: 'default',
delegation: {
compaction: 'fast',
memory_extraction: 'default',
classification: 'complex',
tool_summarisation: 'default',
complex_reasoning: 'complex',
},
maxDelegationDepth: 10,
compaction: {
thresholdPct: 80,
keepTurns: 1,
summaryMaxTokens: 128,
importanceThreshold: 1,
},
});
const result = await orchestrator.compact();
expect(result).not.toBeNull();
expect(sessionCompact).toHaveBeenCalledWith(expect.objectContaining({
session_id: 'session-compact-audit',
messages_before: 6,
messages_after: 3,
}));
} finally {
initAuditLogger(previousAuditLogger as unknown as AuditLogger);
}
});
});
describe('reset()', () => { describe('reset()', () => {
it('clears primary agent conversation history', async () => { it('clears primary agent conversation history', async () => {
const orchestrator = new AgentOrchestrator({ const orchestrator = new AgentOrchestrator({
+2 -2
View File
@@ -310,8 +310,8 @@ export class AgentOrchestrator {
if (this._session) { if (this._session) {
auditLogger?.sessionCompact({ auditLogger?.sessionCompact({
session_id: this._session.id, session_id: this._session.id,
messages_before: result.tokensBefore, messages_before: messages.length,
messages_after: result.tokensAfter, messages_after: result.messages.length,
tokens_before: result.tokensBefore, tokens_before: result.tokensBefore,
tokens_after: result.tokensAfter, tokens_after: result.tokensAfter,
}); });