fix(audit): resolve lint global, compaction metrics, and nudge id
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user