From 948d589ac331ae614c53f564a389cca7e0315ea2 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 21:54:12 -0800 Subject: [PATCH] fix(audit): resolve lint global, compaction metrics, and nudge id --- .../2026-02-16-codebase-audit-report.md | 3 + docs/plans/state.json | 15 ++++ eslint.config.js | 1 + src/backends/native/agent.test.ts | 5 +- src/backends/native/agent.ts | 11 ++- src/backends/native/orchestrator.test.ts | 78 +++++++++++++++++++ src/backends/native/orchestrator.ts | 4 +- 7 files changed, 106 insertions(+), 11 deletions(-) diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index d8d2047..c67d330 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -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-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-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 diff --git a/docs/plans/state.json b/docs/plans/state.json index b0a5b1d..4265ebc 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2463,6 +2463,21 @@ "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" + }, + "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": { diff --git a/eslint.config.js b/eslint.config.js index cded34a..d114f92 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -116,6 +116,7 @@ export default [ WebSocket: 'readonly', location: 'readonly', URLSearchParams: 'readonly', + FileReader: 'readonly', navigator: 'readonly', alert: 'readonly', confirm: 'readonly', diff --git a/src/backends/native/agent.test.ts b/src/backends/native/agent.test.ts index c8dd997..4893e60 100644 --- a/src/backends/native/agent.test.ts +++ b/src/backends/native/agent.test.ts @@ -201,9 +201,8 @@ describe('NativeAgent tool loop', () => { callCount++; // After nudge message, model should respond with text const lastMsg = req.messages[req.messages.length - 1]; - const hasNudge = typeof lastMsg?.content !== 'string' && - Array.isArray(lastMsg?.content) && - lastMsg.content.some((b: any) => b.content?.includes('do NOT call it again')); + const hasNudge = typeof lastMsg?.content === 'string' + && lastMsg.content.includes('do NOT call it again'); if (hasNudge) { return { content: 'Here is what I found from my searches.', diff --git a/src/backends/native/agent.ts b/src/backends/native/agent.ts index dee8cca..cf95b41 100644 --- a/src/backends/native/agent.ts +++ b/src/backends/native/agent.ts @@ -339,18 +339,17 @@ export class NativeAgent { // 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 // that endlessly retry searches with slight query variations. + let nudgeMessage: string | null = null; if (sameToolStreak >= maxSameToolStreak && !nudged) { nudged = true; - toolResultBlocks.push({ - 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, - }); + 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.`; } // Add tool results as a user message 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 if (consecutiveRepeats >= maxConsecutiveRepeats) { diff --git a/src/backends/native/orchestrator.test.ts b/src/backends/native/orchestrator.test.ts index 303bc86..40b05c6 100644 --- a/src/backends/native/orchestrator.test.ts +++ b/src/backends/native/orchestrator.test.ts @@ -8,6 +8,8 @@ import { MemoryStore } from '../../memory/store.js'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; +import { auditLogger, initAuditLogger } from '../../audit/index.js'; +import type { AuditLogger } from '../../audit/index.js'; describe('AgentOrchestrator', () => { 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()', () => { it('clears primary agent conversation history', async () => { const orchestrator = new AgentOrchestrator({ diff --git a/src/backends/native/orchestrator.ts b/src/backends/native/orchestrator.ts index 23be6ad..0564a92 100644 --- a/src/backends/native/orchestrator.ts +++ b/src/backends/native/orchestrator.ts @@ -310,8 +310,8 @@ export class AgentOrchestrator { if (this._session) { auditLogger?.sessionCompact({ session_id: this._session.id, - messages_before: result.tokensBefore, - messages_after: result.tokensAfter, + messages_before: messages.length, + messages_after: result.messages.length, tokens_before: result.tokensBefore, tokens_after: result.tokensAfter, });