diff --git a/apps/api/src/routes/chat.ts b/apps/api/src/routes/chat.ts index 64392fdf9..7d16424e5 100644 --- a/apps/api/src/routes/chat.ts +++ b/apps/api/src/routes/chat.ts @@ -7,7 +7,7 @@ * See: specs/018-project-first-architecture/tasks.md (T027) */ import type { ChatSessionTaskEmbed } from '@simple-agent-manager/shared'; -import { isTaskExecutionStep, isTaskMode } from '@simple-agent-manager/shared'; +import { DEFAULT_SAM_HISTORY_LOAD_LIMIT, isTaskExecutionStep, isTaskMode } from '@simple-agent-manager/shared'; import { and, eq, inArray } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/d1'; import { Hono } from 'hono'; @@ -28,6 +28,16 @@ const chatRoutes = new Hono<{ Bindings: Env }>(); chatRoutes.use('/*', requireAuth(), requireApproved()); +function getSessionMessageLimit(env: Env, requestedLimit?: string): number { + const configuredLimit = parseInt(env.SAM_HISTORY_LOAD_LIMIT || '', 10); + const maxLimit = Number.isFinite(configuredLimit) && configuredLimit > 0 + ? configuredLimit + : DEFAULT_SAM_HISTORY_LOAD_LIMIT; + const parsedLimit = parseInt(requestedLimit || '', 10); + const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : maxLimit; + return Math.min(limit, maxLimit); +} + /** * GET /api/projects/:projectId/sessions * List chat sessions for a project. @@ -108,7 +118,7 @@ chatRoutes.get('/:sessionId', async (c) => { throw errors.notFound('Chat session'); } - const limit = Math.min(parseInt(c.req.query('limit') || '1000', 10), 5000); + const limit = getSessionMessageLimit(c.env, c.req.query('limit')); const beforeParam = c.req.query('before'); const before = beforeParam ? parseInt(beforeParam, 10) : null; diff --git a/apps/api/tests/unit/routes/chat-session-agent-routing.test.ts b/apps/api/tests/unit/routes/chat-session-agent-routing.test.ts index c947747db..3e6d36010 100644 --- a/apps/api/tests/unit/routes/chat-session-agent-routing.test.ts +++ b/apps/api/tests/unit/routes/chat-session-agent-routing.test.ts @@ -17,6 +17,7 @@ vi.mock('drizzle-orm/d1', () => ({ })); vi.mock('@simple-agent-manager/shared', () => ({ + DEFAULT_SAM_HISTORY_LOAD_LIMIT: 200, DEFAULT_WORKSPACE_PROFILE: 'full', isTaskExecutionStep: () => true, isTaskMode: (v: unknown) => v === 'task' || v === 'conversation', @@ -187,6 +188,53 @@ describe('chatRoutes agent session routing', () => { expect(body.session.agentType).toBe('claude-code'); }); + it('caps session detail message loads to the configured history limit', async () => { + mocks.listAcpSessions.mockResolvedValue({ + sessions: [], + total: 0, + }); + + const response = await app.request( + '/api/projects/proj-1/sessions/chat-1?limit=5000', + { method: 'GET' }, + { + DATABASE: {} as D1Database, + SAM_HISTORY_LOAD_LIMIT: '25', + } as Env, + ); + + expect(response.status).toBe(200); + expect(mocks.getMessages).toHaveBeenCalledWith( + expect.anything(), + 'proj-1', + 'chat-1', + 25, + null, + ); + }); + + it('uses the default history limit when no session detail limit is requested', async () => { + mocks.listAcpSessions.mockResolvedValue({ + sessions: [], + total: 0, + }); + + const response = await app.request( + '/api/projects/proj-1/sessions/chat-1', + { method: 'GET' }, + { DATABASE: {} as D1Database } as Env, + ); + + expect(response.status).toBe(200); + expect(mocks.getMessages).toHaveBeenCalledWith( + expect.anything(), + 'proj-1', + 'chat-1', + 200, + null, + ); + }); + it('returns null agentType when ACP session has no agentType', async () => { mocks.listAcpSessions.mockResolvedValue({ sessions: [ diff --git a/tasks/backlog/2026-05-01-tail-worker-log-ingest-auth.md b/tasks/backlog/2026-05-01-tail-worker-log-ingest-auth.md new file mode 100644 index 000000000..6d7172e26 --- /dev/null +++ b/tasks/backlog/2026-05-01-tail-worker-log-ingest-auth.md @@ -0,0 +1,24 @@ +# Tail worker log ingest endpoint requires auth + +## Problem + +The production tail worker forwards Worker logs to `POST /api/admin/observability/logs/ingest`, but that internal service-binding request is being rejected with `Authentication required`. + +This generates thousands of `request_error` entries from `sam-api-prod` and pollutes Cloudflare Workers Observability, making real production errors harder to isolate. + +## Context + +Discovered while investigating production chat session load failures on 2026-05-01. Cloudflare Workers Observability showed repeated `request_error` events for: + +- `POST /api/admin/observability/logs/ingest` +- error: `Authentication required` +- source route: tail worker service binding request from `sam-tail-worker-prod` + +The chat UI failure had a separate root cause: oversized ProjectData DO RPC message responses. + +## Acceptance Criteria + +- [ ] Tail worker log ingestion succeeds without end-user authentication for internal service-binding calls. +- [ ] The ingest route still rejects browser/user-originated unauthenticated requests. +- [ ] A regression test covers the internal ingest path. +- [ ] Production/staging observability no longer shows repeated `Authentication required` errors for `/api/admin/observability/logs/ingest`.