Skip to content

refactor: extract SessionInitInput type and initializeSession#587

Open
ColeMurray wants to merge 1 commit intomainfrom
refactor/extract-initialize-session
Open

refactor: extract SessionInitInput type and initializeSession#587
ColeMurray wants to merge 1 commit intomainfrom
refactor/extract-initialize-session

Conversation

@ColeMurray
Copy link
Copy Markdown
Owner

@ColeMurray ColeMurray commented May 2, 2026

Summary

  • Introduces a shared SessionInitInput interface and initializeSession() function in session/initialize.ts that both handleCreateSession and handleSpawnChild now call
  • Enforces D1-first ordering as an invariant — D1 index is written before the DO is initialized (previously handleSpawnChild had DO-first ordering, which could leave orphaned DOs on D1 failure)
  • Closes the untyped DO init body gap — the SessionInitInput type is shared between the router and the DO handler, preventing field drift
  • Uses domain-meaningful identity naming: participantUserId (session protocol identity for the creator) and platformUserId (canonical user for analytics attribution)

What changed in router.ts

Both handleCreateSession (~217 lines) and handleSpawnChild (~221 lines) had duplicate D1 write + DO init sequences with untyped JSON.stringify bodies. Each now builds a SessionInitInput from its own preconditions and calls initializeSession(). Handler-specific logic (identity resolution, enrichment, guardrails, prompt enqueue) stays in the handlers.

Net effect

Metric Before After
DO init body type shared? No — convention only Yes — SessionInitInput
D1/DO ordering consistent? No — create=D1-first, spawn=DO-first Yes — D1-first always
Lines in router.ts ~2,200 ~2,087
Independently testable? No Yes — initializeSession has 8 unit tests

Test plan

  • 8 new unit tests for initializeSession covering: D1-before-DO ordering, D1 failure prevents DO init, DO init failure throws, correct fields to D1 and DO, correlation headers, return value, branch fallback logic
  • All 1044 control-plane tests pass (including existing router and lifecycle handler tests)
  • Typecheck passes
  • Lint passes

Summary by CodeRabbit

  • Refactor

    • Consolidated core session initialization logic into a dedicated helper module, improving code organization and reducing duplication across session creation and child-spawning workflows.
  • Tests

    • Added comprehensive test suite validating session initialization workflows, including failure handling and request/response payload verification.
  • Documentation

    • Enhanced internal endpoint documentation with clearer specifications of request structure and field mappings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

A new initializeSession helper module extracts the two-step session creation flow (D1 index write followed by Durable Object init) from the router. The router's handleCreateSession and handleSpawnChild now delegate to this helper instead of orchestrating those steps inline, with comprehensive tests validating the order and error paths.

Changes

Session Initialization Extraction

Layer / File(s) Summary
Data Shape
packages/control-plane/src/session/initialize.ts
SessionInitInput interface defines all required session metadata (IDs, repo/model config, user identity, SCM credentials, lineage/spawn fields).
Core Implementation
packages/control-plane/src/session/initialize.ts
initializeSession enforces D1-first ordering: writes session index record with computed baseBranch and timestamps, then POST to Durable Object init endpoint, handling non-OK responses with logged errors and thrown exceptions.
Router Integration
packages/control-plane/src/router.ts
handleCreateSession and handleSpawnChild refactored to construct SessionInitInput and invoke initializeSession, replacing prior inline D1 + DO init calls with guarded error handling.
Documentation & Contracts
packages/control-plane/src/session/http/handlers/session-lifecycle.handler.ts, packages/control-plane/src/session/contracts.test.ts
/internal/init endpoint request body documented; contract test extended to verify SessionInternalPaths.init usage in the new initialize module.
Tests
packages/control-plane/src/session/initialize.test.ts
Comprehensive Vitest suite validates D1→DO call ordering, D1 failure prevention, DO error handling, field mapping (nullability, derived baseBranch), request headers (trace/request IDs, Content-Type), and base-branch fallback logic (defaultBranch"main").

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • open-inspect

🐰 A session springs to life, in order true—
D1 first, then Object, no queue!
The router steps back, our helper takes the stage,
With tests and grace, we write the page.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: extraction of a new SessionInitInput type and initializeSession function into a dedicated module, which is the core refactoring objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/extract-initialize-session

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 9 minutes and 28 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

})
);

if (!initResponse.ok) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing the session-index row before DO init is fine, but on this error path we now leave that freshly inserted row behind. That is a regression for spawned children: handleListChildren() reads directly from D1, so a failed child init will show up as a phantom child stuck in created even though its DO never finished initializing. Could we compensate here by deleting the row or marking it failed before rethrowing?

Copy link
Copy Markdown
Contributor

@open-inspect open-inspect Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR extracts the shared session-initialization flow into session/initialize.ts and adds focused unit coverage around the new helper. The refactor is directionally good, but it introduces a child-session correctness regression on the DO-init failure path.

PR Title and number: refactor: extract SessionInitInput type and initializeSession (#587)
Author: @ColeMurray
Files changed count, additions/deletions: 5 files, +390/-113

Critical Issues

  • [Functionality] packages/control-plane/src/session/initialize.ts:128 - initializeSession() now inserts the session-index row before DO init and then throws on a non-OK init response without compensating the D1 write. For spawned children, handleListChildren() reads directly from D1, so a failed child init will now appear as a phantom child stuck in created. We should delete the row or mark it failed before propagating the error.

Suggestions

  • None beyond the blocking issue above.

Nitpicks

  • None.

Positive Feedback

  • The extraction removes a large amount of duplicated init wiring from router.ts while keeping the create/spawn-specific preconditions local to each handler.
  • The new initializeSession tests cover ordering, failure handling, correlation headers, and branch fallback clearly.
  • Preserving the D1-first invariant for both call sites makes the init flow easier to reason about.

Questions

  • None.

Verdict

Request Changes

Validation performed: focused control-plane tests passed for src/session/initialize.test.ts, src/session/contracts.test.ts, and src/session/http/handlers/session-lifecycle.handler.test.ts.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/control-plane/src/session/initialize.ts`:
- Around line 128-137: The DO init failure path currently just logs and throws,
leaving the earlier-persisted D1 session row orphaned; update the failure
handling around initResponse (in initialize.ts) to perform D1 compensation: call
the D1 delete/remove method for input.sessionId (or the existing session delete
helper) and await its completion before throwing, log success or any delete
error via logger.error/ logger.info (include session_id and trace_id), and if
the delete itself fails still throw the original initialization error so callers
see the init failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6c171ca8-81aa-433c-9249-fbb3a780a80f

📥 Commits

Reviewing files that changed from the base of the PR and between 26f77fe and 1d19910.

📒 Files selected for processing (5)
  • packages/control-plane/src/router.ts
  • packages/control-plane/src/session/contracts.test.ts
  • packages/control-plane/src/session/http/handlers/session-lifecycle.handler.ts
  • packages/control-plane/src/session/initialize.test.ts
  • packages/control-plane/src/session/initialize.ts

Comment on lines +128 to +137
if (!initResponse.ok) {
const errorText = await initResponse.text().catch(() => "unknown");
logger.error("DO init failed", {
session_id: input.sessionId,
status: initResponse.status,
error: errorText,
trace_id: ctx.trace_id,
});
throw new Error(`Failed to initialize session DO: ${initResponse.status}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle DO-init failure with D1 compensation to avoid orphaned created sessions.

Lines 69-85 persist the D1 record first, but Lines 128-137 only throw on DO init failure. That leaves stale index rows (especially harmful for child-session listing/status flows).

💡 Suggested fix
 export async function initializeSession(
   env: Env,
   input: SessionInitInput,
   ctx: RequestContext
 ): Promise<{ sessionId: string; status: string }> {
@@
-  const initResponse = await stub.fetch(
-    new Request(buildSessionInternalUrl(SessionInternalPaths.init), {
-      method: "POST",
-      headers,
-      body: JSON.stringify({
-        sessionName: input.sessionId,
-        repoOwner: input.repoOwner,
-        repoName: input.repoName,
-        repoId: input.repoId,
-        defaultBranch: input.defaultBranch,
-        branch: input.branch,
-        title: input.title,
-        model: input.model,
-        reasoningEffort: input.reasoningEffort,
-        userId: input.participantUserId,
-        scmLogin: input.scmLogin,
-        scmName: input.scmName,
-        scmEmail: input.scmEmail,
-        scmTokenEncrypted: input.scmTokenEncrypted,
-        scmRefreshTokenEncrypted: input.scmRefreshTokenEncrypted,
-        scmTokenExpiresAt: input.scmTokenExpiresAt,
-        scmUserId: input.scmUserId,
-        codeServerEnabled: input.codeServerEnabled,
-        sandboxSettings: input.sandboxSettings,
-        parentSessionId: input.parentSessionId,
-        spawnSource: input.spawnSource,
-        spawnDepth: input.spawnDepth,
-      }),
-    })
-  );
+  let initResponse: Response;
+  try {
+    initResponse = await stub.fetch(
+      new Request(buildSessionInternalUrl(SessionInternalPaths.init), {
+        method: "POST",
+        headers,
+        body: JSON.stringify({
+          sessionName: input.sessionId,
+          repoOwner: input.repoOwner,
+          repoName: input.repoName,
+          repoId: input.repoId,
+          defaultBranch: input.defaultBranch,
+          branch: input.branch,
+          title: input.title,
+          model: input.model,
+          reasoningEffort: input.reasoningEffort,
+          userId: input.participantUserId,
+          scmLogin: input.scmLogin,
+          scmName: input.scmName,
+          scmEmail: input.scmEmail,
+          scmTokenEncrypted: input.scmTokenEncrypted,
+          scmRefreshTokenEncrypted: input.scmRefreshTokenEncrypted,
+          scmTokenExpiresAt: input.scmTokenExpiresAt,
+          scmUserId: input.scmUserId,
+          codeServerEnabled: input.codeServerEnabled,
+          sandboxSettings: input.sandboxSettings,
+          parentSessionId: input.parentSessionId,
+          spawnSource: input.spawnSource,
+          spawnDepth: input.spawnDepth,
+        }),
+      })
+    );
+  } catch (error) {
+    await sessionStore.updateStatus(input.sessionId, "failed").catch((statusError) => {
+      logger.error("Failed to mark session failed after DO init transport error", {
+        session_id: input.sessionId,
+        error: statusError instanceof Error ? statusError.message : String(statusError),
+        trace_id: ctx.trace_id,
+      });
+    });
+    throw error;
+  }
 
   if (!initResponse.ok) {
+    await sessionStore.updateStatus(input.sessionId, "failed").catch((statusError) => {
+      logger.error("Failed to mark session failed after DO init non-ok", {
+        session_id: input.sessionId,
+        error: statusError instanceof Error ? statusError.message : String(statusError),
+        trace_id: ctx.trace_id,
+      });
+    });
     const errorText = await initResponse.text().catch(() => "unknown");
     logger.error("DO init failed", {
       session_id: input.sessionId,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/control-plane/src/session/initialize.ts` around lines 128 - 137, The
DO init failure path currently just logs and throws, leaving the
earlier-persisted D1 session row orphaned; update the failure handling around
initResponse (in initialize.ts) to perform D1 compensation: call the D1
delete/remove method for input.sessionId (or the existing session delete helper)
and await its completion before throwing, log success or any delete error via
logger.error/ logger.info (include session_id and trace_id), and if the delete
itself fails still throw the original initialization error so callers see the
init failure.

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