Skip to content

Comments

feat: add getSessionEvents tRPC endpoint to cloud-agent-next#415

Open
jeanduplessis wants to merge 1 commit intomainfrom
jdp/security-agent-next-phase-1
Open

feat: add getSessionEvents tRPC endpoint to cloud-agent-next#415
jeanduplessis wants to merge 1 commit intomainfrom
jdp/security-agent-next-phase-1

Conversation

@jeanduplessis
Copy link
Contributor

@jeanduplessis jeanduplessis commented Feb 20, 2026

Summary

  • Add getSessionEvents tRPC query endpoint to the cloud-agent-next worker, enabling server-side consumers to retrieve stored execution events via HTTP (without WebSocket)
  • Add corresponding getSessionEvents() method and types to CloudAgentNextClient in the Next.js app
  • Phase 1 of the security agent migration from cloud-agent to cloud-agent-next (see plans/security-agent-migrate-cloud-agent-next.md)

Changes

Worker (cloud-agent-next/)

File Change
src/persistence/CloudAgentSession.ts Add queryEvents() public RPC method delegating to eventQueries.findByFilters()
src/router/schemas.ts Add GetSessionEventsInput, StoredEventSchema, GetSessionEventsOutput zod schemas
src/router/handlers/session-management.ts Add getSessionEvents query handler with protectedProcedure auth and withDORetry
src/router.test.ts 11 unit tests: retrieval, filter forwarding, default/custom limits, auth, error propagation, user isolation

Client (src/lib/cloud-agent-next/)

File Change
cloud-agent-client.ts Add GetSessionEventsInput, StoredEvent types; add getSessionEvents entry to TRPC client type; add getSessionEvents() method with Sentry capture

Design

  • Auth: Uses protectedProcedure (same as getSession) — requires valid user token
  • User isolation: DO keyed by userId:sessionId — users can only query their own sessions
  • Filter API: Accepts cloudAgentSessionId (required), eventTypes, executionId, fromId, limit (default 500, max 1000)
  • Singular executionId → array: Converts to executionIds: [id] for the DO's findByFilters, simpler for callers
  • No router.ts change needed: Handler auto-registers via createSessionManagementHandlers() spread

Verification

  • cloud-agent-next typecheck: passes
  • cloud-agent-next unit tests: 639/639 pass (11 new)
  • Root typecheck: no new errors (pre-existing customLlmRequest.ts failures only)

Deployment

This is purely additive — a new query endpoint with no changes to existing behavior. Zero risk to current consumers. Should be deployed to the Cloudflare Worker before the main security agent migration PR (Phase 2+).

Plan# Security Agent: Migrate from cloud-agent to cloud-agent-next

Background

The security agent's Tier 2 sandbox analysis currently uses the old cloud-agent SSE-based streaming API (client.initiateSessionStream()). This needs to migrate to cloud-agent-next, which uses a two-step prepareSession + initiateFromPreparedSession pattern with callback-based completion notifications instead of SSE streaming.

Current integration (3 import points)

File Import Role
src/lib/security-agent/services/analysis-service.ts createCloudAgentClient Creates old cloud-agent client for Tier 2 sandbox analysis
src/lib/security-agent/services/analysis-service.ts StreamEvent, SystemKilocodeEvent Old SSE stream event types consumed in processAnalysisStream
src/routers/security-agent-router.ts getGitHubTokenForUser Shared GitHub helper (lives in old directory, stays unchanged)
src/routers/organizations/organization-security-agent-router.ts getGitHubTokenForOrganization Shared GitHub helper (lives in old directory, stays unchanged)

Current flow (Tier 2 only)

startSecurityAnalysis()
  → createCloudAgentClient(authToken)
  → client.initiateSessionStream({ githubRepo, githubToken, prompt, mode, model })
  → Returns AsyncGenerator<StreamEvent>
  → Fire-and-forget: processAnalysisStream()
      → Consumes SSE stream events:
          'status'      → capture cloudAgentSessionId, update DB
          'kilocode'    → capture cliSessionId from session_created event, update DB
          'error'       → mark analysis failed
          'interrupted' → mark analysis failed
          'complete'    → fetch result from R2 blob via cli_sessions table
      → Fetch from R2: cli_sessions.ui_messages_blob_url → getBlobContent()
      → Parse RawCliMessage[] → extract completion_result or last text message
      → Tier 3: extractSandboxAnalysis() → direct LLM call
      → Store analysis, attempt auto-dismiss

Target flow

startSecurityAnalysis()
  → createCloudAgentNextClient(authToken)
  → client.prepareSession({
      prompt, mode: 'code', model, githubRepo, githubToken,
      kilocodeOrganizationId,
      callbackTarget: { url, headers }
    })
  → Returns { cloudAgentSessionId, kiloSessionId }
  → Update DB with both session IDs
  → client.initiateFromPreparedSession({ cloudAgentSessionId })
  → Returns immediately (fire-and-forget)

  ... cloud-agent-next executes the analysis ...

  → cloud-agent-next POSTs ExecutionCallbackPayload to callbackTarget.url
  → Callback handler (new internal API route):
      → Validates secret header
      → On 'completed': fetch events via getSessionEvents, extract result, run Tier 3
      → On 'failed'/'interrupted': mark analysis failed
      → Store analysis, attempt auto-dismiss

Phase 1: Add getSessionEvents tRPC endpoint to cloud-agent-next worker

The cloud-agent-next worker stores all execution events in DO SQLite via eventQueries (cloud-agent-next/src/session/queries/events.ts), but this is only accessible through the WebSocket /stream endpoint. A new tRPC query is needed for server-side consumers to retrieve events without WebSocket.

1.1 Add public RPC method to CloudAgentSession DO

File: cloud-agent-next/src/persistence/CloudAgentSession.ts

Add a new public method that wraps this.eventQueries.findByFilters():

async queryEvents(filters: {
  fromId?: number;
  executionIds?: string[];
  eventTypes?: string[];
  startTime?: number;
  endTime?: number;
  limit?: number;
}): Promise<StoredEvent[]> {
  return this.eventQueries.findByFilters(filters);
}

The eventQueries field is currently private (line ~86). The new public method delegates to it without exposing the field directly.

1.2 Add tRPC handler in session-management

File: cloud-agent-next/src/router/handlers/session-management.ts

Add a getSessionEvents query to createSessionManagementHandlers():

  • Input: cloudAgentSessionId (required), eventTypes (optional string array), executionId (optional string), limit (optional number, default 500, max 1000)
  • Auth: protectedProcedure (same as getSession)
  • Implementation: builds DO key from userId:sessionId, calls stub.queryEvents(filters) via withDORetry
  • Output: array of StoredEvent objects

1.3 Add input/output schemas

File: cloud-agent-next/src/router/schemas.ts

Add GetSessionEventsInput and GetSessionEventsOutput zod schemas.

1.4 Register in router

File: cloud-agent-next/src/router.ts

The handler is returned from createSessionManagementHandlers(), so it registers automatically — no change needed to router.ts.

1.5 Add getSessionEvents method to CloudAgentNextClient

File: src/lib/cloud-agent-next/cloud-agent-client.ts

Add:

  • GetSessionEventsInput type (mirrors the worker schema)
  • StoredEvent type (matches cloud-agent-next/src/websocket/types.ts:143-156)
  • getSessionEvents entry in CloudAgentNextTRPCClient type
  • getSessionEvents(input) method on CloudAgentNextClient class
// Type
type StoredEvent = {
  id: number;
  execution_id: string;
  session_id: string;
  stream_event_type: string;
  payload: string;   // JSON stringified
  timestamp: number;
};

// Client method
async getSessionEvents(input: GetSessionEventsInput): Promise<StoredEvent[]> {
  return await this.client.getSessionEvents.query(input);
}

1.6 Write tests

File: cloud-agent-next/src/router/handlers/session-management.test.ts (or new test file)

Test the getSessionEvents endpoint with:

  • Valid session with events → returns filtered events
  • Session not found → appropriate error
  • Empty filters → returns all events (up to default limit)
  • EventTypes filter → returns only matching types

Phase 2: Create internal callback endpoint

2.1 Create the callback route

New file: src/app/api/internal/security-analysis-callback/[findingId]/route.ts

This endpoint receives the ExecutionCallbackPayload from cloud-agent-next when the sandbox analysis completes, fails, or is interrupted.

Pattern: follows existing internal API convention (see src/app/api/internal/code-review-status/[reviewId]/route.ts):

import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { INTERNAL_API_SECRET } from '@/lib/config.server';
import { captureException } from '@sentry/nextjs';

type ExecutionCallbackPayload = {
  sessionId: string;
  cloudAgentSessionId: string;
  executionId: string;
  status: 'completed' | 'failed' | 'interrupted';
  errorMessage?: string;
  kiloSessionId?: string;
};

export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ findingId: string }> }
) {
  // 1. Validate X-Internal-Secret header
  const secret = req.headers.get('X-Internal-Secret');
  if (secret !== INTERNAL_API_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { findingId } = await params;
  const payload: ExecutionCallbackPayload = await req.json();

  // 2. Validate required fields
  // 3. Dispatch based on status:
  //    - 'completed' → handleAnalysisCompleted(findingId, payload)
  //    - 'failed' | 'interrupted' → handleAnalysisFailed(findingId, payload)
  // 4. Return { success: true }
}

2.2 Implement handleAnalysisCompleted

When status is 'completed':

  1. Look up the finding from DB to get metadata (model, owner, userId, correlationId, organizationId) — these need to be stored when the analysis starts (see Phase 3)
  2. Create a CloudAgentNextClient using a service-level auth token (or the stored auth token)
  3. Call client.getSessionEvents({ cloudAgentSessionId, eventTypes: ['kilocode'] }) to fetch kilocode events
  4. Parse the event payloads to find the completion result (look for message.updated or message.part.updated events with the final assistant text)
  5. Extract the raw markdown result
  6. Run Tier 3: extractSandboxAnalysis() — unchanged from current code
  7. Store the structured analysis via updateAnalysisStatus(findingId, 'completed', { analysis })
  8. Attempt auto-dismiss if isExploitable === false
  9. Track PostHog completion event

2.3 Implement handleAnalysisFailed

When status is 'failed' or 'interrupted':

  1. Update finding: updateAnalysisStatus(findingId, 'failed', { error: payload.errorMessage })
  2. Track PostHog failure event

2.4 Store analysis context for callback retrieval

The callback endpoint needs context that was available during startSecurityAnalysis but isn't in the callback payload. Two approaches:

Option A (simpler): Store a JSON blob in the security_findings table when analysis starts — add/reuse a column (e.g., analysis_context) containing { model, userId, authToken, correlationId, organizationId, owner }. The callback handler reads this.

Option B: Encode minimal context in the callback URL path or query params (e.g., /api/internal/security-analysis-callback/[findingId]?model=...&userId=...). Less clean but avoids schema changes.

Recommendation: Option A. The security_findings table already stores analysis JSON with similar metadata. We can store the needed context (model, userId, organizationId, correlationId) in the existing analysis JSON field when starting the analysis. The callback handler then reads it back from the finding.

For the auth token specifically: since the callback may arrive minutes later (after the original request has returned), the auth token from the original request may be expired. The callback handler should generate a fresh service-level API token using generateApiToken() for the user stored in the analysis context, or use a system-level token if Tier 3 extraction supports it.


Phase 3: Rewrite Tier 2 in analysis-service.ts

3.1 Replace client creation and session initiation

File: src/lib/security-agent/services/analysis-service.ts

Replace lines 709-747 (the entire Tier 2 section):

Old code (lines 724-747):

const client = createCloudAgentClient(authToken);
const streamGenerator = client.initiateSessionStream({
  githubRepo, githubToken, kilocodeOrganizationId: organizationId,
  prompt, mode: 'code', model,
});
void processAnalysisStream(findingId, streamGenerator, model, owner, ...);

New code:

import { createCloudAgentNextClient } from '@/lib/cloud-agent-next/cloud-agent-client';
import { APP_URL } from '@/lib/constants';
import { INTERNAL_API_SECRET } from '@/lib/config.server';

const client = createCloudAgentNextClient(authToken);

const callbackUrl = `${APP_URL}/api/internal/security-analysis-callback/${findingId}`;

const { cloudAgentSessionId, kiloSessionId } = await client.prepareSession({
  prompt,
  mode: 'code',
  model,
  githubRepo,
  githubToken,
  kilocodeOrganizationId: organizationId,
  callbackTarget: {
    url: callbackUrl,
    headers: { 'X-Internal-Secret': INTERNAL_API_SECRET },
  },
});

// Store session IDs immediately (before initiation)
await updateAnalysisStatus(findingId, 'running', {
  sessionId: cloudAgentSessionId,
  cliSessionId: kiloSessionId,
});

await client.initiateFromPreparedSession({ cloudAgentSessionId });

3.2 Store analysis context for callback

When updating the finding to 'pending' with partial analysis (line 714-721), include the context the callback handler will need:

const partialAnalysis: SecurityFindingAnalysis = {
  triage,
  analyzedAt: new Date().toISOString(),
  modelUsed: model,
  triggeredByUserId: user.id,
  correlationId,
};
await updateAnalysisStatus(findingId, 'pending', { analysis: partialAnalysis });

The callback handler can retrieve model, triggeredByUserId, correlationId from finding.analysis — these are already stored. The organizationId and owner can be derived from the finding's ownership fields. This means no schema changes needed — the existing analysis JSON has everything.

3.3 Handle prepareSession / initiateFromPreparedSession errors

Wrap both calls in try/catch:

  • prepareSession failure → updateAnalysisStatus(findingId, 'failed', { error })
  • initiateFromPreparedSession failure → same, plus clean up the prepared session via client.deleteSession(cloudAgentSessionId)
  • InsufficientCreditsError → propagate up (same as current behavior)

Phase 4: Delete dead code

4.1 Remove processAnalysisStream function

File: src/lib/security-agent/services/analysis-service.ts (lines 352-557)

This ~200-line function is entirely replaced by the callback handler. Delete it.

4.2 Remove fetchLastAssistantMessage function

File: src/lib/security-agent/services/analysis-service.ts (lines 143-237)

This function fetches results from R2 blobs via the old cli_sessions table. No longer needed — results come from getSessionEvents via the callback handler. Delete it.

4.3 Remove helper types and functions

File: src/lib/security-agent/services/analysis-service.ts

  • RawCliMessage type (lines 119-127) — R2 blob message format, no longer needed
  • getCliMessageContent function (lines 132-137) — R2 blob helper, no longer needed
  • isSessionCreatedEvent function (lines 108-110) — old SSE event helper, no longer needed

4.4 Remove old imports

File: src/lib/security-agent/services/analysis-service.ts

Remove:

import { createCloudAgentClient } from '@/lib/cloud-agent/cloud-agent-client';
import type { StreamEvent, SystemKilocodeEvent } from '@/components/cloud-agent/types';
import { cliSessions } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { getBlobContent } from '@/lib/r2/cli-sessions';

4.5 Move finalizeAnalysis to callback handler

The finalizeAnalysis function (lines 249-339) contains the Tier 3 extraction + storage + auto-dismiss logic. This logic needs to move into (or be called from) the callback handler's handleAnalysisCompleted. It can remain as an exported function in analysis-service.ts and be imported by the callback route, or be moved to a shared module.


Phase 5: Update frontend session links

5.1 Verify link format compatibility

The /cloud/chat?sessionId= URL already auto-routes between old and new UIs based on the session ID prefix:

  • Old cloud-agent session IDs: no ses_ prefix → routes to old CloudChatPageWrapper
  • cloud-agent-next session IDs: ses_ prefix → routes to new CloudChatPageWrapperNext

File: src/app/(app)/cloud/chat/page.tsx (line 14): isNewSession(sessionId) checks for ses_ prefix.

Since prepareSession returns a kiloSessionId with a ses_ prefix, the existing link format already works — no URL structure change needed.

5.2 Update security findings to use kiloSessionId

The cli_session_id field in security_findings currently stores the old cli session ID (UUID format). After migration, it will store the kiloSessionId from prepareSession (which has ses_ prefix). The frontend components that read cli_session_id to construct the link (FindingDetailDialog.tsx:114,269-270,315-316 and AnalysisJobsCard.tsx:302,307-308) don't need changes — they just pass the ID as a query param.

Verify: no changes needed to:

  • src/components/security-agent/FindingDetailDialog.tsx
  • src/components/security-agent/AnalysisJobsCard.tsx

Phase 6: Extract result from cloud-agent-next events

The callback handler needs to extract the final analysis text from the events returned by getSessionEvents. The event format in cloud-agent-next differs from the old R2 blob format.

6.1 Understand the event structure

Events in cloud-agent-next have stream_event_type values like:

  • kilocode — Kilocode CLI structured events (session lifecycle, message updates, etc.)
  • complete — Execution completed
  • output — stdout/stderr
  • error — Error occurred

The payload field is a JSON string. For kilocode events, the payload contains nested event data following the OpenCode SDK message format (Message + Part[]).

6.2 Implement result extraction

Create a helper function (e.g., extractResultFromEvents) in the security agent service layer:

  1. Call getSessionEvents({ cloudAgentSessionId, eventTypes: ['kilocode'], limit: 1000 })
  2. Parse each event's payload JSON
  3. Look for events where the parsed data indicates a completed assistant message with text content
  4. Extract the final text content (the analysis markdown)

The exact payload parsing logic needs to be derived from how the EventProcessor (src/lib/cloud-agent-next/processor/event-processor.ts) assembles messages from events. The processor handles events like message.updated, message.part.updated, session.status, etc.

Alternatively, since the security agent only needs the final text result (not real-time streaming), a simpler approach may work:

  • Filter events for the complete event type
  • Look at the last few kilocode events before the complete event
  • Parse those for the assistant's final text output

This extraction logic should be developed alongside Phase 1 testing, using real event data to validate the parsing.


Summary of files changed

File Change
cloud-agent-next/src/persistence/CloudAgentSession.ts Add queryEvents() public RPC method
cloud-agent-next/src/router/handlers/session-management.ts Add getSessionEvents tRPC query handler
cloud-agent-next/src/router/schemas.ts Add GetSessionEventsInput/Output schemas
src/lib/cloud-agent-next/cloud-agent-client.ts Add StoredEvent type, getSessionEvents() method, update CloudAgentNextTRPCClient
src/app/api/internal/security-analysis-callback/[findingId]/route.ts New — callback endpoint
src/lib/security-agent/services/analysis-service.ts Major rewrite: replace old client + SSE stream with cloud-agent-next prepare+initiate+callback; delete ~400 lines of dead code
src/components/security-agent/FindingDetailDialog.tsx Verify only — links should work as-is with ses_ prefixed session IDs
src/components/security-agent/AnalysisJobsCard.tsx Verify only — same as above

Files NOT changed

File Reason
src/routers/security-agent-router.ts getGitHubTokenForUser is a shared utility, not cloud-agent-specific
src/routers/organizations/organization-security-agent-router.ts Same — getGitHubTokenForOrganization stays
src/lib/security-agent/services/triage-service.ts Tier 1 uses direct LLM calls, no cloud-agent
src/lib/security-agent/services/extraction-service.ts Tier 3 uses direct LLM calls, no cloud-agent
DB schema for security_findings Existing session_id and cli_session_id columns reused with new values

Risks and considerations

  1. Auth token lifetime: The callback may arrive minutes after the original request. The stored authToken may be expired. The callback handler should generate a fresh token using generateApiToken() for the user found in the analysis context (stored in finding.analysis.triggeredByUserId). This requires loading the user from the DB in the callback handler.

  2. Event payload parsing: The exact format of kilocode events in cloud-agent-next needs to be validated against real execution data. The EventProcessor is complex (~460 lines) because it handles streaming deltas. The security agent only needs the final result, so a simpler parser should suffice, but it needs to be tested with actual events.

  3. Reliability improvement: The callback pattern with Cloudflare Queue (5 retries, exponential backoff) is more reliable than the current SSE stream, which can silently fail if the Next.js process loses the connection.

  4. cloud-agent-next worker deployment: Phase 1 requires deploying changes to the cloud-agent-next Cloudflare Worker. This should be deployed and verified before the Next.js changes go live.

  5. Feature flag / gradual rollout: Consider gating the migration behind a feature flag or rolling it out per-organization. This allows fallback to the old cloud-agent path if issues arise. The triage-only path (Tier 1) is unaffected and continues working regardless.

  6. Backwards compatibility of session links: Old findings analyzed before the migration will still have old-format session IDs in cli_session_id. The /cloud/chat page already handles both formats (line 14 of page.tsx checks isNewSession), so old links continue working.


PR and Deployment Strategy

This migration spans two independently deployed systems (the cloud-agent-next Cloudflare Worker and the Next.js app), so deployment order matters.

PR 1: Add getSessionEvents to cloud-agent-next worker

Scope: Phases 1.1–1.4, 1.6 (DO method, tRPC handler, schemas, tests)

Files changed:

  • cloud-agent-next/src/persistence/CloudAgentSession.ts
  • cloud-agent-next/src/router/handlers/session-management.ts
  • cloud-agent-next/src/router/schemas.ts
  • Tests

Deploy: Goes out first to the Cloudflare Worker. Purely additive — a new query endpoint with no changes to existing behavior. Zero risk to current consumers.

Verification: Call the new endpoint against an existing session to confirm it returns events correctly.

PR 2: Add getSessionEvents to CloudAgentNextClient

Scope: Phase 1.5 (client-side types and method)

Files changed:

  • src/lib/cloud-agent-next/cloud-agent-client.ts

Deploy: Merges to the Next.js app. Also purely additive — adds a method nobody calls yet. Can be deployed independently once PR 1's worker deploy is live.

Note: PR 2 could be bundled into PR 3, but keeping it separate makes reviews smaller and lets you verify the client method works in isolation.

PR 3: Security agent migration (main PR)

Scope: Phases 2, 3, 4, 5, 6

Files changed:

  • src/app/api/internal/security-analysis-callback/[findingId]/route.ts (new)
  • src/lib/security-agent/services/analysis-service.ts (major rewrite)
  • Possibly a helper for event result extraction

Prerequisite: PR 1 deployed to the worker, PR 2 merged.

This is the critical PR. It switches the security agent from old cloud-agent to cloud-agent-next. Everything in this PR is behind the existing Tier 2 code path (only runs when forceSandbox || triage.needsSandboxAnalysis), so Tier 1 triage-only analyses are completely unaffected.

Deploy: Standard Next.js deploy. Once live, all new Tier 2 analyses use cloud-agent-next.

Verification:

  1. Trigger an analysis on a finding that requires sandbox analysis (either via forceSandbox: true or a finding that triage routes to Tier 2)
  2. Confirm the callback endpoint receives the completion notification
  3. Confirm the result is extracted and stored correctly
  4. Confirm the "View agent session" link in the UI opens the correct cloud-agent-next chat view

Deployment order

PR 1 (worker) ──deploy──→ cloud-agent-next worker live with getSessionEvents
                              │
PR 2 (client)  ──merge──→    │  (can happen in parallel with PR 1 deploy)
                              │
                              ▼
PR 3 (migration) ──merge + deploy──→ Security agent uses cloud-agent-next

PR 1 must be deployed before PR 3 goes live. PR 2 must be merged before PR 3. PR 1 and PR 2 have no ordering dependency on each other (they change different codebases), but PR 1's deploy must be live before anyone calls getSessionEvents.

Rollback plan

  • PR 3 rollback: Revert the Next.js deploy. The old cloud-agent is still running and the old code path works. Findings that had analysis started via cloud-agent-next during the brief window will show as failed (callback arrives but the handler is gone) — these can be re-analyzed.
  • PR 1 is not revertible independently once PR 3 is live (the callback handler depends on getSessionEvents). But PR 1 is additive-only, so there's no reason to revert it.

Feature flag consideration

The migration could be gated behind a feature flag (e.g., security-agent-cloud-agent-next) in startSecurityAnalysis. The flag would control which client to use — old createCloudAgentClient + SSE stream path vs new createCloudAgentNextClient + callback path. This adds code complexity (both paths coexist temporarily) but allows per-org or percentage-based rollout. Given that the security agent is relatively low-traffic (runs per-finding, not per-request), a flag may be overkill — but it's available if the team prefers it.

Cleanup PR (optional, after confidence)

Once the migration is verified in production:

  • Remove the feature flag if one was added
  • Consider whether the getGitHubTokenForUser/getGitHubTokenForOrganization helpers should be moved out of the cloud-agent/ directory into a shared location (they're used by both old and new consumers)

Add an HTTP-based query endpoint for retrieving stored execution events
from the Durable Object's SQLite storage. This enables server-side
consumers (e.g. security agent callback handlers) to fetch events
without requiring a WebSocket connection.

Worker changes:
- Add queryEvents() public RPC method to CloudAgentSession DO
- Add getSessionEvents tRPC query with protectedProcedure auth
- Add GetSessionEventsInput/Output zod schemas with validation

Client changes:
- Add StoredEvent and GetSessionEventsInput types
- Add getSessionEvents() method to CloudAgentNextClient

Tests:
- 11 unit tests covering retrieval, filter forwarding, auth,
  error propagation, and user isolation
@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 20, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

This PR adds a well-structured getSessionEvents tRPC endpoint for server-side event retrieval from CloudAgentSession Durable Objects. Key observations:

  • Security: Uses protectedProcedure with proper user isolation via userId:sessionId DO key lookup ✅
  • Schema validation: Zod input/output schemas correctly match the StoredEvent type from websocket/types.ts
  • Retry handling: Properly uses withDORetry for resilient DO communication ✅
  • Pagination: Sensible defaults (limit: 500, max: 1000) with cursor-based pagination via fromId
  • Test coverage: Comprehensive tests covering successful retrieval, filter forwarding, authorization, error handling, and user isolation ✅
  • Client integration: Client-side types and method follow existing patterns ✅
Files Reviewed (5 files)
  • cloud-agent-next/src/persistence/CloudAgentSession.ts - Added queryEvents() method to DO
  • cloud-agent-next/src/router.test.ts - Tests for new endpoint
  • cloud-agent-next/src/router/handlers/session-management.ts - New getSessionEvents handler
  • cloud-agent-next/src/router/schemas.ts - Input/output Zod schemas
  • src/lib/cloud-agent-next/cloud-agent-client.ts - Client-side types and method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant