Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions apps/api/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +28,16 @@

chatRoutes.use('/*', requireAuth(), requireApproved());

function getSessionMessageLimit(env: Env, requestedLimit?: string): number {
const configuredLimit = parseInt(env.SAM_HISTORY_LOAD_LIMIT || '', 10);

Check warning on line 32 in apps/api/src/routes/chat.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ3k4mIKDB_MdPLcVGQj&open=AZ3k4mIKDB_MdPLcVGQj&pullRequest=874
const maxLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
? configuredLimit
: DEFAULT_SAM_HISTORY_LOAD_LIMIT;
const parsedLimit = parseInt(requestedLimit || '', 10);

Check warning on line 36 in apps/api/src/routes/chat.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ3k4mIKDB_MdPLcVGQk&open=AZ3k4mIKDB_MdPLcVGQk&pullRequest=874
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.
Expand Down Expand Up @@ -108,7 +118,7 @@
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;

Expand Down
48 changes: 48 additions & 0 deletions apps/api/tests/unit/routes/chat-session-agent-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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: [
Expand Down
24 changes: 24 additions & 0 deletions tasks/backlog/2026-05-01-tail-worker-log-ingest-auth.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading