Skip to content

Threads 8: Add externally owned thread state - #262

Open
FranciscoMoretti wants to merge 3 commits into
mainfrom
codex/threads-v2-08-external-state
Open

Threads 8: Add externally owned thread state#262
FranciscoMoretti wants to merge 3 commits into
mainfrom
codex/threads-v2-08-external-state

Conversation

@FranciscoMoretti

@FranciscoMoretti FranciscoMoretti commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • add AbstractThread as the tree-aware equivalent of AI SDK AbstractChat
  • define the ThreadState subscription/update contract and provide MemoryThreadState
  • keep Thread as the default in-memory implementation
  • allow useThread({ thread }) to subscribe to externally owned thread instances
  • document the package architecture and state ownership boundary

Verification

  • bun --cwd packages/thread test
  • bun --cwd packages/thread build
  • bun --cwd packages/thread test:types
  • bun --cwd packages/thread lint

Summary by Sourcery

Introduce an abstract, state-agnostic thread controller and make Thread use pluggable thread state to support externally owned thread instances.

New Features:

  • Add AbstractThread as a framework-independent controller for tree-backed chat threads.
  • Define a ThreadState interface and provide MemoryThreadState as the default in-memory implementation.
  • Allow Thread to be constructed with an injected ThreadState so useThread can subscribe to externally owned thread instances.

Enhancements:

  • Refactor Thread to extend AbstractThread and delegate tree and run orchestration through the new state abstraction.
  • Clarify architecture documentation around Thread, AbstractThread, ThreadState, and React subscription boundaries.
  • Tighten type-level checks in use-thread tests to enforce correct state ownership and hook usage.

Documentation:

  • Expand ARCHITECTURE.md to document the ThreadState contract, AbstractThread responsibilities, and state ownership model.

Tests:

  • Add unit tests for injected ThreadState behavior and synchronous update requirements.
  • Update useThread tests to cover the new transport handling and external Thread usage patterns.

Summary by cubic

Adds externally owned thread state with ThreadState and MemoryThreadState, and refactors Thread to extend a new AbstractThread. Apps can keep the message tree in their own store; useThread now reads from the thread’s external state.

  • New Features

    • Introduced AbstractThread for framework-agnostic orchestration; Thread is the default adapter.
    • Added ThreadState interface and default MemoryThreadState; updates are atomic and must be synchronous.
    • Exported AbstractThread, MemoryThreadState, ThreadState, and AbstractThreadInit; updated docs and types.
  • Migration

    • No changes for existing in-memory usage.
    • To own state externally, pass state to new Thread({ state }); do not also pass messages or initialTree.
    • Custom ThreadState.update must call its updater exactly once, synchronously.

Written for commit a7aabe5. Summary will update on new commits.

Review in cubic

Stack

  1. Threads 7: Rename the thread controller #261
  2. Threads 8: Add externally owned thread state #262 👈 current
  3. Threads 9: Store ordered thread snapshots in ChatJS #240
  4. Threads 10: Mount useThread in ChatJS #241
  5. Threads 11: Add branch navigation and retry #242
  6. Threads 12: Isolate branch stream lifecycles #243
  7. Threads 13: Stream follow-up parallel responses #244
  8. Threads 14: Add cancellable request gates #263
  9. Threads 15: Gate first-message parallel runs #245
  10. Threads 16: Stop newly provisioned thread runs #246
  11. Threads 17: Show parallel response lifecycle states #247
  12. Threads 18: Publish installable thread sources #248
  13. Threads 19: Add the thread playground model #249
  14. Threads 20: Add the interactive thread playground #250
  15. Threads 21: Publish the threads product page #251
  16. Threads 22: Add the value-first package guide #252
  17. Threads 23: Document ChatJS threaded behavior #253
  18. Threads 24: Prepare the thread package release #254

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-js-docs Ready Ready Preview, Comment Jul 27, 2026 5:55pm
chat-js-site Ready Ready Preview, Comment Jul 27, 2026 5:55pm
sparka Ready Ready Preview, Comment Jul 27, 2026 5:55pm

Request Review

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce an AbstractThread controller that orchestrates runs against a pluggable ThreadState, keep Thread as the default in-memory implementation via MemoryThreadState, and update docs/tests/types so useThread can subscribe to externally owned thread state safely.

File-Level Changes

Change Details Files
Refactor Thread into an AbstractThread that drives AI SDK runs against a pluggable ThreadState while Thread becomes a thin concrete wrapper that chooses MemoryThreadState by default.
  • Extract most of Thread’s logic into a new AbstractThread class, including tree navigation, run orchestration, tool handling, and message sending/regeneration APIs.
  • Introduce a ThreadState interface and MemoryThreadState implementation to own observable tree snapshots and enforce synchronous, single-call updates.
  • Refactor Thread to extend AbstractThread and resolve its state via resolveThreadState, wiring initialTree/messages into MemoryThreadState when no external state is provided.
  • Move MessageTree construction and snapshot building from Thread into AbstractThread/MemoryThreadState so operational state (runs, transports) stays in the controller while serializable state lives in ThreadState.
packages/thread/src/thread.ts
packages/thread/src/abstract-thread.ts
packages/thread/src/thread-state.ts
Adjust types, run-host wiring, and exports to support AbstractThread, external ThreadState ownership, and updated AI SDK integration.
  • Add ThreadState and AbstractThreadInit types, make ThreadInit a union that disallows mixing external state with initialTree/messages, and thread these types into the rest of the module.
  • Update ThreadRunHost to depend on AbstractThreadInit fields and modify ai-sdk-run-chat usage to accept the new host definition.
  • Export AbstractThread, MemoryThreadState, AbstractThreadInit, and ThreadState from the public index for consumers.
  • Tighten ThreadState.update semantics by having AbstractThread enforce exactly-one synchronous invocation per update call.
packages/thread/src/types.ts
packages/thread/src/ai-sdk-run-chat.ts
packages/thread/src/index.ts
Update React integration and docs to describe the new architecture and external state ownership boundary, including how useThread works with externally supplied Thread instances and ThreadState.
  • Revise ARCHITECTURE.md to document AbstractThread, ThreadState, MemoryThreadState, and how useThread subscribes via ThreadState, clarifying what lives in controller vs. state.
  • Adjust use-thread snapshot management to keep a stable ref containing both the thread and its latest snapshot when the Thread instance changes.
  • Clarify in docs that useThread accepts a Thread controller and that callbacks/transport updates do not replace the thread, its state, or active runs.
packages/thread/ARCHITECTURE.md
packages/thread/src/use-thread.ts
Extend tests and type checks to cover externally injected ThreadState implementations, enforcement of synchronous updates, and useThread’s behavior with external threads and transports.
  • Add RecordingThreadState test helper and tests ensuring Thread publishes via an injected ThreadState and rejects non-synchronous update implementations.
  • Add use-thread-types checks that validate external state ownership constraints and the correct useThread signature, including ts-expect-error for invalid combinations.
  • Update useThread tests to align with new semantics, including passing thread into renderUseThread and correcting ChatTransport.sendMessages typing and test naming.
packages/thread/test/thread.test.ts
packages/thread/test/use-thread-types.ts
packages/thread/test/use-thread.test.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c0910ef1-61ab-43fd-8e42-4a6472c9a603

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/threads-v2-08-external-state

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

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/thread/test/use-thread.test.ts" line_range="183" />
<code_context>
+	describe("useThread", () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a runtime test that `useThread` can work with an externally owned Thread/state instance.

The types guarantee that `useThread` accepts an externally owned `Thread` with a custom `ThreadState`, but there’s no runtime test covering this. Please add a test that:

- Constructs a `Thread` with a custom `ThreadState` (e.g. `new Thread({ state: new MemoryThreadState({ messages: [...] }) })`).
- Passes that thread into `renderUseThread({ thread })`.
- Verifies the hook snapshot reflects the external messages/cursor, and that changes via `thread.setCursor(...)` / `thread.sendMessage(...)` are observed by the hook.

This confirms the hook correctly subscribes to and reacts to externally owned thread state, not just at the type level.
</issue_to_address>

### Comment 2
<location path="packages/thread/ARCHITECTURE.md" line_range="187" />
<code_context>
 - the selected cursor
-- run records and per-run status
+- public run status and errors
+- the selected-path and aggregate projections
+- the immutable snapshot consumed by React
+
</code_context>
<issue_to_address>
**nitpick (typo):** The phrase "selected-path" would read more naturally as "selected path".

Unless "selected-path" is a defined term that must be hyphenated, please change it to "selected path" for standard usage and to match the parallel structure with "aggregate projections."

```suggestion
- the selected path and aggregate projections
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

transport,
});
const hook = renderUseThread({ thread, resume: true });
const hook = renderUseThread({ resume: true, thread });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a runtime test that useThread can work with an externally owned Thread/state instance.

The types guarantee that useThread accepts an externally owned Thread with a custom ThreadState, but there’s no runtime test covering this. Please add a test that:

  • Constructs a Thread with a custom ThreadState (e.g. new Thread({ state: new MemoryThreadState({ messages: [...] }) })).
  • Passes that thread into renderUseThread({ thread }).
  • Verifies the hook snapshot reflects the external messages/cursor, and that changes via thread.setCursor(...) / thread.sendMessage(...) are observed by the hook.

This confirms the hook correctly subscribes to and reacts to externally owned thread state, not just at the type level.

- the selected cursor
- run records and per-run status
- public run status and errors
- the selected-path and aggregate projections

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): The phrase "selected-path" would read more naturally as "selected path".

Unless "selected-path" is a defined term that must be hyphenated, please change it to "selected path" for standard usage and to match the parallel structure with "aggregate projections."

Suggested change
- the selected-path and aggregate projections
- the selected path and aggregate projections

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR separates thread orchestration from observable state ownership.

  • Adds AbstractThread for tree and request orchestration.
  • Introduces the injectable ThreadState contract and default MemoryThreadState implementation.
  • Refactors Thread to use the new abstractions while preserving the public controller API.
  • Allows useThread to subscribe to externally owned Thread instances.
  • Expands architecture documentation and tests for external state ownership.

Confidence Score: 3/5

The PR is not yet safe to merge because creating another controller for a shared ThreadState can make subscribers lose the first controller's active-run state.

A new Thread still initializes an empty controller-local run registry and immediately publishes its values into the injected state, replacing active run metadata and reporting ready while another controller using that state may still be processing.

Files Needing Attention: packages/thread/src/abstract-thread.ts

Important Files Changed

Filename Overview
packages/thread/src/abstract-thread.ts Introduces state-agnostic orchestration, but constructor publication still lets a fresh controller overwrite run fields belonging to another controller sharing the state.
packages/thread/src/thread-state.ts Adds the synchronous observable state contract's default in-memory implementation.
packages/thread/src/thread.ts Refactors Thread into a thin AbstractThread adapter that resolves injected or in-memory state.
packages/thread/src/types.ts Defines the external state contract and mutually exclusive initialization options.
packages/thread/src/use-thread.ts Retains external Thread snapshot subscription behavior under the new ownership model.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Hook["useThread"] --> Controller["Thread"]
  Controller --> Abstract["AbstractThread"]
  Abstract --> State["ThreadState"]
  Abstract --> Registry["Controller-local RunRegistry"]
  State --> Snapshot["Observable tree and run snapshot"]
  State --> Subscribers["React and external subscribers"]
  Registry --> Snapshot
Loading

Reviews (2): Last reviewed commit: "feat(thread): support externally owned t..." | Re-trigger Greptile

Comment on lines +106 to +109
this.#runs = new RunRegistry(options.concurrency);
this.#state = options.state;
this.#runHost = this.createRunHost();
this.publish();

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.

P1 Shared run state gets erased

When a second Thread is created with the same external ThreadState, its fresh empty RunRegistry is immediately published over the shared snapshot, causing subscribers to see no active runs and a ready status while the original controller is still processing.

Knowledge Base Used: @chatjs/thread Package

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

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.

3 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/thread/src/abstract-thread.ts">

<violation number="1" location="packages/thread/src/abstract-thread.ts:277">
P1: After `restore`, persisted tool calls and approval requests cannot accept `addToolOutput` or `addToolApprovalResponse`: clearing the registry drops ownership and restored messages never reconstruct it. Rebuild ownership and lazily create the owning branch adapter before routing these helpers.

(Based on your team's feedback about restored tool ownership.) [FEEDBACK_USED]</violation>

<violation number="2" location="packages/thread/src/abstract-thread.ts:571">
P2: Selecting a persisted root assistant makes `resumeStream()` silently return rather than reconnect because root nodes have `parentMessageId: null`. Allow null parent IDs when creating the reconstructed run; `ThreadRunSpec` already supports them.</violation>
</file>

<file name="packages/thread/ARCHITECTURE.md">

<violation number="1" location="packages/thread/ARCHITECTURE.md:187">
P3: "selected-path" reads as a hyphenated compound modifier, but here it's used as a noun phrase parallel to "aggregate projections." Removing the hyphen ("selected path") would match the parallel structure.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


restore(snapshot: MessageTreeSnapshot<TMessage>) {
this.assertCanResetTree();
this.#runs.clear();

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.

P1: After restore, persisted tool calls and approval requests cannot accept addToolOutput or addToolApprovalResponse: clearing the registry drops ownership and restored messages never reconstruct it. Rebuild ownership and lazily create the owning branch adapter before routing these helpers.

(Based on your team's feedback about restored tool ownership.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/thread/src/abstract-thread.ts, line 277:

<comment>After `restore`, persisted tool calls and approval requests cannot accept `addToolOutput` or `addToolApprovalResponse`: clearing the registry drops ownership and restored messages never reconstruct it. Rebuild ownership and lazily create the owning branch adapter before routing these helpers.

(Based on your team's feedback about restored tool ownership.) </comment>

<file context>
@@ -0,0 +1,673 @@
+
+	restore(snapshot: MessageTreeSnapshot<TMessage>) {
+		this.assertCanResetTree();
+		this.#runs.clear();
+		this.updateTree((tree) => tree.restore(snapshot));
+	}
</file context>

if (!message || message.role !== "assistant") {
return;
}
const parentMessageId = tree.getParentId(messageId);

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.

P2: Selecting a persisted root assistant makes resumeStream() silently return rather than reconnect because root nodes have parentMessageId: null. Allow null parent IDs when creating the reconstructed run; ThreadRunSpec already supports them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/thread/src/abstract-thread.ts, line 571:

<comment>Selecting a persisted root assistant makes `resumeStream()` silently return rather than reconnect because root nodes have `parentMessageId: null`. Allow null parent IDs when creating the reconstructed run; `ThreadRunSpec` already supports them.</comment>

<file context>
@@ -0,0 +1,673 @@
+		if (!message || message.role !== "assistant") {
+			return;
+		}
+		const parentMessageId = tree.getParentId(messageId);
+		if (!parentMessageId) return;
+
</file context>

- the selected cursor
- run records and per-run status
- public run status and errors
- the selected-path and aggregate projections

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.

P3: "selected-path" reads as a hyphenated compound modifier, but here it's used as a noun phrase parallel to "aggregate projections." Removing the hyphen ("selected path") would match the parallel structure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/thread/ARCHITECTURE.md, line 187:

<comment>"selected-path" reads as a hyphenated compound modifier, but here it's used as a noun phrase parallel to "aggregate projections." Removing the hyphen ("selected path") would match the parallel structure.</comment>

<file context>
@@ -133,22 +138,67 @@ React hooks or components.
 - the selected cursor
-- run records and per-run status
+- public run status and errors
+- the selected-path and aggregate projections
+- the immutable snapshot consumed by React
+
</file context>
Suggested change
- the selected-path and aggregate projections
- the selected path and aggregate projections

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