Skip to content

Comments

Dedicated Per-thread Workers for Thread Syncing#1842

Closed
jonahseguin wants to merge 6 commits intoMail-0:stagingfrom
jonahseguin:feature/thread-sync-worker
Closed

Dedicated Per-thread Workers for Thread Syncing#1842
jonahseguin wants to merge 6 commits intoMail-0:stagingfrom
jonahseguin:feature/thread-sync-worker

Conversation

@jonahseguin
Copy link

@jonahseguin jonahseguin commented Jul 27, 2025

Description

Adds a new binding for the SyncThreadWorker Durable Object. This Durable Object is designed as a short-lived execution context for syncing an individual mail thread from a provider into R2.

This improves the opportunity for increased parallelization, and critically avoids memory limit issues when syncing many/large threads. Since each Durable Object has a memory limit of 128mb, this allows large threads and large quantities of threads to be synced more reliably.


Type of Change

  • ⚡ Performance improvement

Areas Affected

  • Email Integration (Gmail, IMAP, etc.)
  • Data Storage/Management

Testing Done

Describe the tests you've done:

  • Manual testing performed

Security Considerations

For changes involving data or authentication:

  • Rate limiting is considered (if applicable)

Checklist

  • I have read the CONTRIBUTING document
  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in complex areas
  • My changes generate no new warnings

Additional Notes

There is potential for other patterns to be explored: such as creating a pool of the worker threads in a size that matches the concurrency number (currently 3) and re-using those durable objects for multiple thread syncs. A potential caveat of this is that memory may not be freed between syncThread() RPC invocations. This is why I chose the pattern of a new Durable Object for each thread sync execution.
The ThreadSyncWorker Durable Object is intentionally designed with no state and no storage to make it as lightweight as possible.

Important note: I removed the threadData property from the returned object in syncThreads() - from my testing and investigating I could not usage of this property.
This is optimal for memory conservation within the ZeroDriver DO.
If this is incorrect, and the threadData property is used/needed, let me know.


By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.


Summary by cubic

Added a dedicated Durable Object worker for each mail thread sync to improve parallelization and prevent memory issues when syncing large or many threads.

  • Performance
    • Each thread sync now runs in its own short-lived worker, avoiding shared memory limits.
    • Removed unused thread data from sync results to reduce memory usage.

Summary by CodeRabbit

  • New Features
    • Introduced a new background worker to handle email thread synchronization, improving reliability and scalability.
  • Bug Fixes
    • Improved handling of label data to prevent parsing errors during thread label updates.
  • Chores
    • Updated configuration to support the new background worker for thread synchronization.

@jazzberry-ai
Copy link

jazzberry-ai bot commented Jul 27, 2025

Bug Report

Name Severity Example test case Description
ThreadSyncWorker Durable Object Concurrency Bottleneck High 1. Create multiple ZeroDriver Durable Objects. 2. For each ZeroDriver, trigger the synchronization of 50-100 email threads concurrently. 3. Observe slow synchronization times and potential request failures. The current implementation uses a single ThreadSyncWorker Durable Object, leading to contention and performance bottlenecks when synchronizing a large number of threads concurrently. THREADS_BUCKET.put is a likely bottleneck. Consider sharding, queueing, or caching.

Comments? Email us. Your free trial ends in 2 days.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 27, 2025

Walkthrough

This change introduces a new ThreadSyncWorker Durable Object for handling thread synchronization, updates the ZeroDriver class to delegate sync operations to this worker, and modifies configuration files to register the new Durable Object. Export statements and field handling in ZeroDriver are updated accordingly.

Changes

Cohort / File(s) Change Summary
apps/server/src/main.ts Imports and exports ThreadSyncWorker, updating the export list.
apps/server/src/routes/agent/index.ts Adds _connection field to ZeroDriver, delegates thread sync to ThreadSyncWorker, updates label parsing logic.
apps/server/src/routes/agent/sync-worker.ts Introduces new ThreadSyncWorker Durable Object for thread synchronization and storage.
apps/server/wrangler.jsonc Registers ThreadSyncWorker Durable Object and migration in local environment configuration.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ZeroDriver
    participant ThreadSyncWorker
    participant KVBucket

    Client->>ZeroDriver: syncThread({ threadId })
    ZeroDriver->>ThreadSyncWorker: syncThread(connectionId, _connection, threadId)
    ThreadSyncWorker->>ZeroDriver: get driver from connection
    ThreadSyncWorker->>ZeroDriver: fetch thread data with retry
    ThreadSyncWorker->>KVBucket: store thread data
    ThreadSyncWorker-->>ZeroDriver: return latest message
    ZeroDriver-->>Client: respond (without thread data)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Possibly related PRs

  • refactor durable objects #1764: Refactors durable objects, splitting ZeroAgent into ZeroDriver and ZeroAgent, which is related through durable object and ZeroDriver context but does not include the new ThreadSyncWorker.

Suggested labels

design, high priority

Poem

🚀
New threads now sync with cosmic might,
A worker spins through digital night.
Durable, scalable, ready to go,
Data in buckets, all in a row.
ZeroDriver delegates with style—
The future of sync, Muskian and wild!

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai bot added design Improvements & changes to design & UX High Priority High Priority Work labels Jul 27, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

1 issue found across 4 files • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

"class_name": "ThinkingMCP",
},
{
"name": "THREAD_SYNC_WORKER",
Copy link
Contributor

Choose a reason for hiding this comment

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

THREAD_SYNC_WORKER binding is only added for the local environment, so env.THREAD_SYNC_WORKER will be undefined in staging and production causing runtime failures.

Prompt for AI agents
Address the following comment on apps/server/wrangler.jsonc at line 51:

<comment>THREAD_SYNC_WORKER binding is only added for the local environment, so `env.THREAD_SYNC_WORKER` will be undefined in staging and production causing runtime failures.</comment>

<file context>
@@ -47,6 +47,10 @@
             &quot;name&quot;: &quot;THINKING_MCP&quot;,
             &quot;class_name&quot;: &quot;ThinkingMCP&quot;,
           },
+          {
+            &quot;name&quot;: &quot;THREAD_SYNC_WORKER&quot;,
+            &quot;class_name&quot;: &quot;ThreadSyncWorker&quot;,
+          },
</file context>

Copy link
Contributor

@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: 10

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad891d and 84b51ef.

📒 Files selected for processing (4)
  • apps/server/src/main.ts (2 hunks)
  • apps/server/src/routes/agent/index.ts (8 hunks)
  • apps/server/src/routes/agent/sync-worker.ts (1 hunks)
  • apps/server/wrangler.jsonc (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.{js,jsx,ts,tsx}: Use 2-space indentation
Use single quotes
Limit lines to 100 characters in width
Semicolons are required

Files:

  • apps/server/src/main.ts
  • apps/server/src/routes/agent/sync-worker.ts
  • apps/server/src/routes/agent/index.ts
**/*.{js,jsx,ts,tsx,css}

📄 CodeRabbit Inference Engine (AGENT.md)

Use Prettier with sort-imports and Tailwind plugins

Files:

  • apps/server/src/main.ts
  • apps/server/src/routes/agent/sync-worker.ts
  • apps/server/src/routes/agent/index.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (AGENT.md)

Enable TypeScript strict mode

Files:

  • apps/server/src/main.ts
  • apps/server/src/routes/agent/sync-worker.ts
  • apps/server/src/routes/agent/index.ts
🧠 Learnings (2)
apps/server/src/routes/agent/sync-worker.ts (1)

Learnt from: retrogtx
PR: #1468
File: apps/server/src/trpc/routes/mail.ts:386-391
Timestamp: 2025-06-27T04:59:29.731Z
Learning: In apps/server/src/trpc/routes/mail.ts, the attachment processing logic conditionally handles mixed attachment types - it preserves existing File-like objects with arrayBuffer methods while only converting serialized attachments that need processing through toAttachmentFiles.

apps/server/src/routes/agent/index.ts (1)

Learnt from: MrgSub
PR: #1837
File: apps/server/src/lib/brain.fallback.prompts.ts:211-217
Timestamp: 2025-07-26T20:39:06.670Z
Learning: In the ThreadLabels prompt system, existing labels should not be automatically preserved. The AI agent should re-evaluate all labels (both existing and new) against the current thread summary and only return labels that currently apply to the thread content, even if it means dropping previously applied labels that are no longer relevant.

🔇 Additional comments (2)
apps/server/src/main.ts (1)

45-45: Good. The import and export are properly wired up.

This correctly integrates the ThreadSyncWorker into the application. Clean and simple.

Also applies to: 858-858

apps/server/src/routes/agent/index.ts (1)

528-528: Good call removing threadData from the return. Less memory usage.

This aligns with the memory optimization goals. The comment is helpful for future maintainers.

export class ZeroDriver extends AIChatAgent<typeof env> {
private foldersInSync: Map<string, boolean> = new Map();
private syncThreadsInProgress: Map<string, boolean> = new Map();
private _connection: typeof connection.$inferSelect | undefined = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Good move adding the connection field, but the naming could be better.

The underscore prefix is old school. Just call it connection and make it readonly to prevent accidental mutations.

-  private _connection: typeof connection.$inferSelect | undefined = undefined;
+  private readonly connection: typeof connection.$inferSelect | undefined = undefined;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/server/src/routes/agent/index.ts at line 69, rename the private field
from `_connection` to `connection` and add the `readonly` modifier to it. This
change improves naming clarity and prevents accidental mutations by making the
field immutable.

Comment on lines +240 to 244
this._connection = await db.query.connection.findFirst({
where: eq(connection.id, this.name),
});
if (_connection) this.driver = connectionToDriver(_connection);
if (this._connection) this.driver = connectionToDriver(this._connection);
this.ctx.waitUntil(conn.end());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

This is problematic. You're assigning to a readonly field after construction.

If we make connection readonly as suggested, this won't work. Either initialize it in the constructor or keep it mutable but add proper null checks everywhere.

Consider this pattern instead:

-      this._connection = await db.query.connection.findFirst({
+      const conn = await db.query.connection.findFirst({
         where: eq(connection.id, this.name),
       });
-      if (this._connection) this.driver = connectionToDriver(this._connection);
+      if (!conn) throw new Error(`Connection not found for ${this.name}`);
+      this._connection = conn;
+      this.driver = connectionToDriver(conn);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._connection = await db.query.connection.findFirst({
where: eq(connection.id, this.name),
});
if (_connection) this.driver = connectionToDriver(_connection);
if (this._connection) this.driver = connectionToDriver(this._connection);
this.ctx.waitUntil(conn.end());
const conn = await db.query.connection.findFirst({
where: eq(connection.id, this.name),
});
if (!conn) throw new Error(`Connection not found for ${this.name}`);
this._connection = conn;
this.driver = connectionToDriver(conn);
this.ctx.waitUntil(conn.end());
🤖 Prompt for AI Agents
In apps/server/src/routes/agent/index.ts around lines 240 to 244, you are
assigning a value to a readonly field _connection after the object construction,
which is not allowed if the field is marked readonly. To fix this, either
initialize _connection during the constructor or remove the readonly modifier
and ensure you add null checks wherever _connection is accessed to handle its
potential absence safely.

Comment on lines +487 to +489
const threadSyncWorker = env.THREAD_SYNC_WORKER.get(env.THREAD_SYNC_WORKER.newUniqueId());
// Sync thread with the worker
const latest = await threadSyncWorker.syncThread(this.name, this._connection, threadId);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Creating a new Durable Object for each thread sync is wasteful.

I get that you want stateless workers, but creating a new DO instance for every single thread is like using a new rocket for every satellite. Consider a worker pool pattern.

The current approach will create thousands of DOs for large sync operations. Consider:

  1. Reusing DO instances with a cleanup mechanism between syncs
  2. Implementing a worker pool pattern with a fixed number of workers
  3. Using the Durable Object ID as a shard key to distribute load

Would you like me to open an issue to track this optimization?

🤖 Prompt for AI Agents
In apps/server/src/routes/agent/index.ts around lines 487 to 489, the code
creates a new Durable Object instance for each thread sync, which is inefficient
and can lead to excessive resource usage. Refactor this by implementing a worker
pool pattern that reuses a fixed number of Durable Object instances. Use the
Durable Object ID as a shard key to distribute load evenly, and add a cleanup
mechanism to reset worker state between syncs. This will optimize resource usage
and improve scalability.

Comment on lines +580 to 590
yield * Effect.sleep(200);

const threadData = yield* Effect.tryPromise(() => self.getWithRetry(threadId));
// Uses the dedicated ThreadSyncWorker durable object
const result = yield * Effect.tryPromise(() => self.syncThread({ threadId }));

if (!threadData || !threadData.latest || !threadData.latest.threadId) {
if (result && result.success) {
return 1 as const;
} else {
console.log(`Failed to sync thread ${threadId}:`, result?.reason || 'Unknown error');
return 0 as const;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

The sleep duration seems arbitrary. Why 200ms?

Magic numbers without explanation are bad. Either make it configurable or add a comment explaining the rationale.

+        // Small delay to prevent rate limiting when processing multiple threads
+        const THREAD_SYNC_DELAY_MS = 200;
-        yield * Effect.sleep(200);
+        yield * Effect.sleep(THREAD_SYNC_DELAY_MS);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
yield * Effect.sleep(200);
const threadData = yield* Effect.tryPromise(() => self.getWithRetry(threadId));
// Uses the dedicated ThreadSyncWorker durable object
const result = yield * Effect.tryPromise(() => self.syncThread({ threadId }));
if (!threadData || !threadData.latest || !threadData.latest.threadId) {
if (result && result.success) {
return 1 as const;
} else {
console.log(`Failed to sync thread ${threadId}:`, result?.reason || 'Unknown error');
return 0 as const;
}
// Small delay to prevent rate limiting when processing multiple threads
const THREAD_SYNC_DELAY_MS = 200;
yield * Effect.sleep(THREAD_SYNC_DELAY_MS);
// Uses the dedicated ThreadSyncWorker durable object
const result = yield * Effect.tryPromise(() => self.syncThread({ threadId }));
if (result && result.success) {
return 1 as const;
} else {
console.log(`Failed to sync thread ${threadId}:`, result?.reason || 'Unknown error');
return 0 as const;
}
🤖 Prompt for AI Agents
In apps/server/src/routes/agent/index.ts around lines 580 to 590, the sleep
duration of 200ms is a magic number without explanation. To fix this, either
replace the hardcoded 200 with a named constant that clearly indicates its
purpose or make the duration configurable via a parameter or configuration
setting. Alternatively, add a comment above the sleep call explaining why 200ms
was chosen to clarify its intent.

let currentLabels: string[];
try {
currentLabels = JSON.parse(result[0].latest_label_ids || '[]') as string[];
currentLabels = JSON.parse(result[0].latest_label_ids?.toString() || '[]') as string[];
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

The toString() fix is good but could be cleaner.

Nice catch on the JSON parsing issue. Consider using optional chaining for even better safety.

-        currentLabels = JSON.parse(result[0].latest_label_ids?.toString() || '[]') as string[];
+        currentLabels = JSON.parse(result[0]?.latest_label_ids?.toString() ?? '[]') as string[];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
currentLabels = JSON.parse(result[0].latest_label_ids?.toString() || '[]') as string[];
currentLabels = JSON.parse(result[0]?.latest_label_ids?.toString() ?? '[]') as string[];
🤖 Prompt for AI Agents
In apps/server/src/routes/agent/index.ts at line 1006, improve the JSON parsing
line by using optional chaining to safely access latest_label_ids before calling
toString(), replacing the current usage with something like
result[0].latest_label_ids?.toString() to avoid errors if latest_label_ids is
undefined or null, ensuring cleaner and safer code.

Comment on lines +14 to +16
private getThreadKey(connectionId: string, threadId: string) {
return `${connectionId}/${threadId}.json`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

This method should be static. It doesn't use any instance state.

Private methods that don't access instance properties should be static for better performance and clarity.

-  private getThreadKey(connectionId: string, threadId: string) {
+  private static getThreadKey(connectionId: string, threadId: string) {
     return `${connectionId}/${threadId}.json`;
   }

And update the usage:

-      this.getThreadKey(connectionId, threadId),
+      ThreadSyncWorker.getThreadKey(connectionId, threadId),
🤖 Prompt for AI Agents
In apps/server/src/routes/agent/sync-worker.ts around lines 14 to 16, the method
getThreadKey does not use any instance state and should be made static. Change
the method declaration to static and update all calls to this method to use the
class name instead of an instance reference to reflect this change.

Comment on lines +18 to +46
public async syncThread(
connectionId: string,
_connection: typeof connection.$inferSelect,
threadId: string,
): Promise<ParsedMessage | undefined> {
// Get driver from connection
const driver = connectionToDriver(_connection);
if (!driver) throw new Error('No driver available');

// Get thread
const thread = await Effect.runPromise(
withRetry(Effect.tryPromise(() => driver!.get(threadId))),
);

// Store thread
await env.THREADS_BUCKET.put(
this.getThreadKey(connectionId, threadId),
JSON.stringify(thread),
{
customMetadata: {
threadId,
},
},
);

// Return latest message in thread
return thread.latest;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for R2 operations and validate inputs.

This method needs better error handling and input validation. What happens if R2 is down? What if threadId is invalid?

   public async syncThread(
     connectionId: string,
     _connection: typeof connection.$inferSelect,
     threadId: string,
   ): Promise<ParsedMessage | undefined> {
+    // Validate inputs
+    if (!connectionId || !threadId) {
+      throw new Error('Invalid connectionId or threadId');
+    }
+
     // Get driver from connection
     const driver = connectionToDriver(_connection);
     if (!driver) throw new Error('No driver available');

     // Get thread
     const thread = await Effect.runPromise(
       withRetry(Effect.tryPromise(() => driver.get(threadId))),
     );

-    // Store thread
-    await env.THREADS_BUCKET.put(
-      ThreadSyncWorker.getThreadKey(connectionId, threadId),
-      JSON.stringify(thread),
-      {
-        customMetadata: {
-          threadId,
-        },
-      },
-    );
+    // Store thread with error handling
+    try {
+      await env.THREADS_BUCKET.put(
+        ThreadSyncWorker.getThreadKey(connectionId, threadId),
+        JSON.stringify(thread),
+        {
+          customMetadata: {
+            threadId,
+            syncedAt: new Date().toISOString(),
+          },
+        },
+      );
+    } catch (error) {
+      console.error(`Failed to store thread ${threadId} in R2:`, error);
+      throw new Error(`R2 storage failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+    }

     // Return latest message in thread
     return thread.latest;
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/server/src/routes/agent/sync-worker.ts lines 18 to 46, the syncThread
method lacks error handling for R2 storage operations and does not validate
inputs like threadId. Add checks to ensure threadId is valid before proceeding.
Wrap the R2 put operation in a try-catch block to handle potential failures such
as R2 downtime, and throw or return meaningful errors if these operations fail.
This will make the method more robust and prevent unhandled exceptions.

Comment on lines +24 to +30
const driver = connectionToDriver(_connection);
if (!driver) throw new Error('No driver available');

// Get thread
const thread = await Effect.runPromise(
withRetry(Effect.tryPromise(() => driver!.get(threadId))),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove the non-null assertion. Handle the error properly.

Using driver! with non-null assertion after checking for null is bad practice. The type system already knows driver is not null inside the Effect.

     // Get thread
     const thread = await Effect.runPromise(
-      withRetry(Effect.tryPromise(() => driver!.get(threadId))),
+      withRetry(Effect.tryPromise(() => driver.get(threadId))),
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const driver = connectionToDriver(_connection);
if (!driver) throw new Error('No driver available');
// Get thread
const thread = await Effect.runPromise(
withRetry(Effect.tryPromise(() => driver!.get(threadId))),
);
const driver = connectionToDriver(_connection);
if (!driver) throw new Error('No driver available');
// Get thread
const thread = await Effect.runPromise(
withRetry(Effect.tryPromise(() => driver.get(threadId))),
);
🤖 Prompt for AI Agents
In apps/server/src/routes/agent/sync-worker.ts around lines 24 to 30, remove the
non-null assertion operator (!) used on the driver variable inside the
Effect.tryPromise call. Since driver is already checked for null and an error is
thrown if it is null, the type system knows driver is not null here. Simply use
driver without the non-null assertion to handle the error properly and improve
code clarity.

Comment on lines +50 to +53
{
"name": "THREAD_SYNC_WORKER",
"class_name": "ThreadSyncWorker",
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

This is broken. You forgot to add ThreadSyncWorker to staging and production.

The ThreadSyncWorker binding is only added to the local environment. This will cause deployment failures in staging and production. We need to move fast and break things, but not literally break production.

Add the missing configuration:

# In staging environment (after line 209):
          {
            "name": "THINKING_MCP",
            "class_name": "ThinkingMCP",
          },
+         {
+           "name": "THREAD_SYNC_WORKER",
+           "class_name": "ThreadSyncWorker",
+         },

# In production environment (after line 374):
          {
            "name": "THINKING_MCP",  
            "class_name": "ThinkingMCP",
          },
+         {
+           "name": "THREAD_SYNC_WORKER",
+           "class_name": "ThreadSyncWorker",
+         },

Also applies to: 189-210, 354-375

🤖 Prompt for AI Agents
In apps/server/wrangler.jsonc around lines 50 to 53, the ThreadSyncWorker
binding is only configured for the local environment, missing in staging and
production which will cause deployment failures. Add the ThreadSyncWorker
binding configuration to the staging and production sections of the file,
ensuring it matches the local environment setup. Also, apply the same fix to the
other specified line ranges 189-210 and 354-375 to maintain consistency across
all environments.

Comment on lines +101 to +104
{
"tag": "v7",
"new_classes": ["ThreadSyncWorker"],
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing migrations in staging and production. This is not good.

The v7 migration for ThreadSyncWorker is only in local. Need to add it to staging and production environments to actually deploy this thing.

Add after the existing migrations in both environments:

# In staging migrations (after line 266):
        {
          "tag": "v7",
          "new_sqlite_classes": ["ThinkingMCP"],
        },
+       {
+         "tag": "v8",
+         "new_classes": ["ThreadSyncWorker"],
+       },

# In production migrations (after line 425):
        {
          "tag": "v7", 
          "new_sqlite_classes": ["ThinkingMCP"],
        },
+       {
+         "tag": "v8",
+         "new_classes": ["ThreadSyncWorker"],
+       },

Also applies to: 238-267, 397-426

🤖 Prompt for AI Agents
In apps/server/wrangler.jsonc around lines 101 to 104 and also in lines 238-267
and 397-426, the v7 migration for ThreadSyncWorker is only added in the local
environment but missing in staging and production. To fix this, add the same v7
migration entry with "new_classes": ["ThreadSyncWorker"] after the existing
migrations in both the staging and production sections to ensure the migration
is deployed across all environments.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 7, 2025

This PR has merge conflicts and has been open for more than 3 days. It will be automatically closed. Please resolve the conflicts and reopen the PR if you'd like to continue working on it.

@github-actions github-actions bot closed this Aug 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design Improvements & changes to design & UX High Priority High Priority Work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant