thedotmack/claude-mem · warning
NotFound
NotFound
Error message
Event not found
What it means
404 from GET /v1/events/:id/observations when no agent_events row matches the id for the caller's team. The lookup is team-scoped (WHERE id = $1 AND team_id = $2), so an event that exists under another team is indistinguishable from one that never existed — deliberate, to avoid revealing existence. A nonexistent id and a cross-tenant id return the same response.
Source
Thrown at src/server/routes/v1/ServerV1PostgresRoutes.ts:452
res.json({ event: serializeEvent(fullEvent) });
}));
// GET /v1/events/:id/observations — list observations linked to event via observation_sources.
// Scope is enforced by joining observations.team_id = $teamId and the
// event ownership check before any rows are returned. Cross-tenant
// requests are reported as 404 to avoid revealing existence.
app.get('/v1/events/:id/observations', readAuth, this.asyncHandler(async (req, res) => {
const teamId = this.requireTeamId(req, res);
if (!teamId) return;
const id = this.routeParam(req.params.id);
const eventResult = await this.options.pool.query(
`SELECT id, project_id FROM agent_events WHERE id = $1 AND team_id = $2`,
[id, teamId],
);
const eventRow = eventResult.rows[0] as undefined | { id: string; project_id: string };
if (!eventRow) {
res.status(404).json({ error: 'NotFound', message: 'Event not found' });
return;
}
if (!this.ensureProjectAllowed(req, res, eventRow.project_id)) return;
const obsResult = await this.options.pool.query(
`
SELECT o.id, o.project_id, o.team_id, o.server_session_id, o.kind, o.content,
o.metadata, o.generation_key, o.created_by_job_id, o.created_at, o.updated_at,
os.id AS source_id_pk, os.source_type, os.source_id, os.generation_job_id, os.created_at AS source_created_at
FROM observation_sources os
INNER JOIN observations o ON o.id = os.observation_id
WHERE os.source_type = 'agent_event'
AND os.source_id = $1
AND o.team_id = $2
AND o.project_id = $3
ORDER BY o.created_at ASC
`,
[eventRow.id, teamId, eventRow.project_id],View on GitHub (pinned to e2d1df569a)
Solutions
- Confirm the event create call returned 201 and use the returned event id verbatim.
- Verify the id belongs to the caller's team (events listing scoped to the key's team).
- Treat 404 as terminal for that id — retrying without fixing the id or scope cannot succeed.
- Wait for ingestion before first poll if the event was just created.
Defensive patterns
Strategy: validation
Validate before calling
// Use the id from the create response, never a hand-copied one
const created = await postJson('/v1/events', eventPayload);
const eventId: string = created.event.id;
const observations = await getJson(`/v1/events/${encodeURIComponent(eventId)}/observations`); Type guard
interface NotFoundBody { error: string; message: string }
function isEventNotFound(res: Response, body: unknown): boolean {
return res.status === 404 && typeof body === 'object' && body !== null &&
(body as NotFoundBody).error === 'NotFound' && (body as NotFoundBody).message === 'Event not found';
} Prevention
- Propagate server-returned ids end-to-end instead of reconstructing them.
- Poll observations only after the create call succeeded.
- Treat 404 as terminal for that id; never blind-retry.
When it happens
Trigger: GET /v1/events/<wrong-or-typo'd-id>/observations; event id from a different team/environment; event not yet ingested when observations are polled right after creation; key's team differs from the event's team.
Common situations: Polling for observations before the create call completed; copying ids between staging and prod; truncating UUIDs during manual testing.
Related errors
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/5f5555d2856e7e4f.
Report an issue: GitHub.