Skip to content

Threads 2: Add the canonical message tree - #235

Merged
FranciscoMoretti merged 6 commits into
mainfrom
codex/threads-v2-02-message-tree
Jul 24, 2026
Merged

Threads 2: Add the canonical message tree#235
FranciscoMoretti merged 6 commits into
mainfrom
codex/threads-v2-02-message-tree

Conversation

@FranciscoMoretti

@FranciscoMoretti FranciscoMoretti commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements canonical message, parent, child, root, and cursor storage.
  • Derives active paths and sibling/leaf navigation without duplicating ancestors.
  • Validates missing parents and cycles while preserving hidden branches.

Behavior

Adds the headless topology layer; no network or React behavior.

Verification

  • Package message-tree tests pass at the stack tip.

Review focus

Tree invariants, snapshot round trips, and path reconciliation.


Summary by cubic

Adds a canonical, headless message tree with navigation, safe mutations, and v1 snapshot save/restore using ordered traversal. Separates path updates from selection and removes root-sentinel collisions; no network or React changes.

  • New Features

    • Topology & navigation: parent/child links with real roots via null parent and cursorId; getPath/getPathIds, getSiblings/getLeaves, getParent/getChildren.
    • Mutations & paths: upsertMessage (optional sibling index, no cross-parent moves), removeLeaf (moves cursor), cursor setters, setPath (update + select), updatePath (update only).
    • Snapshots v1: ordered-node serialization via getSnapshot() using a single tree traversal and strict restore (rejects unordered/duplicate nodes).
    • Utilities: getMessageText, getIndexes().
  • Refactors & Bug Fixes

    • Removed root sentinel usage so IDs like __root__ work; getIndexes().rootIds shows real roots.
    • Separated path updates from selection; tightened validation for cycles and cross-parent moves.
    • Reused a single ordered tree traversal for leaves and snapshot serialization for consistent ordering.
    • Scripts: include test in format/lint; exported getMessageText.

Written for commit 81c2cb5. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added getMessageText to extract combined text from message parts, and re-exported it from the thread package.
    • Enhanced the threaded message tree API with setPath / updatePath for selected-path management.
  • Bug Fixes
    • Strengthened tree validation to prevent cycles, reject invalid parent insertions, and restrict removeLeaf to leaf nodes.
    • Ensured removal and path operations update cursor/path state consistently.
  • Tests
    • Added Bun test coverage for cursor/path behavior, snapshot round-trips, and validation errors.
  • Chores
    • Expanded formatting and lint checks to include test files and additional docs/configs.

Stack

  1. Threads 0: Define the useThread target architecture #233
  2. Threads 22: Adopt AI SDK response identity lifecycle #258
  3. Threads 1: Define useThread package contracts #234
  4. Threads 2: Add the canonical message tree #235 👈 current
  5. Threads 3: Adapt isolated AI SDK runs #236
  6. Threads 4: Orchestrate concurrent tree runs #237
  7. Threads 5: Prove AI SDK behavioral parity #238
  8. Threads 6: Expose the useChat-compatible useThread hook #239
  9. Threads 10: Store ordered thread snapshots in ChatJS #240
  10. Threads 11: Mount useThread in ChatJS #241
  11. Threads 12: Add branch navigation and retry #242
  12. Threads 13: Isolate branch stream lifecycles #243
  13. Threads 14: Stream follow-up parallel responses #244
  14. Threads 16: Gate first-message parallel runs #245
  15. Threads 17: Stop newly provisioned thread runs #246
  16. Threads 18: Show parallel response lifecycle states #247
  17. Threads 19: Publish installable thread sources #248
  18. Threads 20: Add the thread playground model #249
  19. Threads 21: Add the interactive thread playground #250
  20. Threads 22: Publish the threads product page #251
  21. Threads 23: Add the value-first package guide #252
  22. Threads 24: Document ChatJS threaded behavior #253
  23. Threads 25: Prepare the thread package release #254

@cursor

cursor Bot commented Jul 16, 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 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.

Sorry @FranciscoMoretti, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Jul 16, 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 24, 2026 7:38am
chat-js-site Ready Ready Preview, Comment Jul 24, 2026 7:38am
sparka Ready Ready Preview, Comment Jul 24, 2026 7:38am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Thread package updates

Layer / File(s) Summary
Tree storage and navigation
packages/thread/src/message-tree.ts
Adds message relationship maps, cloned lookups, traversal methods, path derivation, leaf discovery, and cursor management.
Mutation and persistence
packages/thread/src/message-tree.ts, packages/thread/test/message-tree.test.ts
Adds validated upserts, leaf removal, path updates, snapshots, restoration, indexes, clearing, path validation, and tests for valid and invalid tree states.
Package utility and validation wiring
packages/thread/src/message-utils.ts, packages/thread/src/index.ts, packages/thread/package.json
Adds getMessageText, re-exports it from the package entry point, and expands format and lint script targets.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the canonical message tree for threads.
✨ 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 codex/threads-v2-02-message-tree

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 490c5c20b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/thread/src/message-tree.ts Outdated
Comment thread packages/thread/src/message-tree.ts Outdated

@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.

All reported issues were addressed across 4 files

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

Re-trigger cubic

Comment thread packages/thread/src/message-tree.ts Outdated
Comment thread packages/thread/src/message-tree.ts Outdated
Comment thread packages/thread/src/message-tree.ts Outdated
Base automatically changed from codex/threads-v2-01-contracts to main July 23, 2026 07:25
@FranciscoMoretti
FranciscoMoretti force-pushed the codex/threads-v2-02-message-tree branch from f7c7b6f to cc47844 Compare July 23, 2026 07:43

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
packages/thread/src/message-tree.ts (2)

177-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated "cannot move" check; inconsistent privacy style on validatePath.

The existing-parent conflict check is duplicated verbatim between upsertMessage (177-182) and validatePath (270-277) — a future edit to one message/format is likely to miss the other. Also, validatePath uses TS private while every other member uses true #private fields; for consistency it should use #validatePath.

♻️ Extract shared check
+	`#assertNotReparenting`(messageId: string, parentId: string | null) {
+		const existingParentId = this.#parentById.get(messageId);
+		if (existingParentId !== undefined && existingParentId !== parentId) {
+			throw new Error(
+				`Cannot move message ${messageId} from ${existingParentId ?? "root"} to ${parentId ?? "root"}`,
+			);
+		}
+	}

Then call this.#assertNotReparenting(message.id, parentId) in both upsertMessage and validatePath (renaming it #validatePath for consistency with the other private fields).

Also applies to: 262-280

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/thread/src/message-tree.ts` around lines 177 - 182, Extract the
duplicated existing-parent conflict validation into a shared
`#assertNotReparenting` method, preserving the current error message and behavior,
then call it from both upsertMessage and validatePath. Rename validatePath to
`#validatePath` and update all references to use the true private method syntax.

63-83: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Recursive traversal risks stack overflow on deep chains.

getLeaves and getSnapshot's visit recurse once per tree depth level. For very long single-branch conversations (long-running threads without forks), this can approach the call-stack limit. Converting to an explicit-stack iterative traversal removes this risk entirely.

♻️ Example iterative rewrite for getLeaves
 	getLeaves(messageId: string | null = null) {
 		const leaves: TMessage[] = [];
-		const visit = (id: string) => {
-			const children = this.#childrenByParentId.get(id) ?? [];
-			if (children.length === 0) {
-				const message = this.#messagesById.get(id);
-				if (message) {
-					leaves.push(clone(message));
-				}
-				return;
-			}
-			for (const childId of children) {
-				visit(childId);
-			}
-		};
-
-		for (const childId of this.#childrenByParentId.get(messageId) ?? []) {
-			visit(childId);
-		}
+		const stack = [...(this.#childrenByParentId.get(messageId) ?? [])];
+		while (stack.length > 0) {
+			const id = stack.pop() as string;
+			const children = this.#childrenByParentId.get(id) ?? [];
+			if (children.length === 0) {
+				const message = this.#messagesById.get(id);
+				if (message) leaves.push(clone(message));
+			} else {
+				stack.push(...children);
+			}
+		}
 		return leaves;
 	}

The same pattern (explicit stack, preserving pre-order for getSnapshot) applies to the visit in getSnapshot.

Also applies to: 108-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/thread/src/message-tree.ts` around lines 63 - 83, Replace the
recursive visit traversal in getLeaves with an explicit stack, preserving leaf
collection order and cloning behavior. Apply the same iterative traversal
pattern to getSnapshot’s visit, pushing children in reverse order where needed
to preserve its existing pre-order output, and remove recursion so deep message
chains cannot overflow the call stack.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/thread/src/message-tree.ts`:
- Around line 177-182: Extract the duplicated existing-parent conflict
validation into a shared `#assertNotReparenting` method, preserving the current
error message and behavior, then call it from both upsertMessage and
validatePath. Rename validatePath to `#validatePath` and update all references to
use the true private method syntax.
- Around line 63-83: Replace the recursive visit traversal in getLeaves with an
explicit stack, preserving leaf collection order and cloning behavior. Apply the
same iterative traversal pattern to getSnapshot’s visit, pushing children in
reverse order where needed to preserve its existing pre-order output, and remove
recursion so deep message chains cannot overflow the call stack.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44b2b814-f34b-4820-a41a-f2d16fab8975

📥 Commits

Reviewing files that changed from the base of the PR and between e3189c5 and cc47844.

📒 Files selected for processing (5)
  • packages/thread/package.json
  • packages/thread/src/index.ts
  • packages/thread/src/message-tree.ts
  • packages/thread/src/message-utils.ts
  • packages/thread/test/message-tree.test.ts

@vercel
vercel Bot temporarily deployed to Preview – chat-js-docs July 23, 2026 08:06 Inactive
@vercel
vercel Bot temporarily deployed to Preview – chat-js-site July 23, 2026 08:06 Inactive
@vercel
vercel Bot temporarily deployed to Preview – sparka July 23, 2026 08:08 Inactive
@FranciscoMoretti
FranciscoMoretti force-pushed the codex/threads-v2-02-message-tree branch from cc47844 to 267e6cb Compare July 23, 2026 08:16
Comment thread packages/thread/src/message-tree.ts Outdated
Comment on lines +220 to +229
mergePath(messages: TMessage[], options: { moveCursor?: boolean } = {}) {
this.validatePath(messages, true);
let parentId: string | null = null;
for (const message of messages) {
this.upsertMessage(message, parentId);
parentId = message.id;
}
if (options.moveCursor ?? true) {
this.#cursorId = messages.at(-1)?.id ?? null;
}

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 mergePath([]) silently resets cursor to null

When messages is an empty array, validatePath and the insertion loop are both no-ops, but messages.at(-1)?.id ?? null evaluates to null, so this.#cursorId is unconditionally overwritten with null. Any caller that passes a dynamically-computed, possibly-empty path will silently lose the active cursor position without any structural change to the tree.

Concretely: if the cursor is "u3" and mergePath([]) is called (e.g. a filtered path returned no messages), the cursor becomes null and getPath() returns [] on the next call, even though the tree is intact.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/thread/src/message-tree.ts (2)

44-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle empty-string message IDs explicitly.

upsertMessage accepts any string ID, including "", but these truthiness checks treat an empty ID as absent. Such a message can be stored and selected, yet getParent() and getPath() return incomplete results. Use explicit null/undefined checks and add an empty-ID test.

Proposed fix
-		return parentId ? this.getMessage(parentId) : undefined;
+		return parentId == null ? undefined : this.getMessage(parentId);

-		if (!messageId) {
+		if (messageId == null) {
			return [];
		}

-		while (currentId) {
+		while (currentId !== null) {

Also applies to: 85-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/thread/src/message-tree.ts` around lines 44 - 47, Update getParent
and the related getPath logic to check parent/message IDs explicitly for null or
undefined rather than by truthiness, so an empty-string ID is preserved and
resolved like any other valid string ID. Add a test covering an empty-ID message
through both parent lookup and path traversal.

234-242: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unsupported snapshot versions before restoration.

getSnapshot() writes version: 1, but restore() ignores the version entirely. A future or incompatible persisted snapshot can therefore be interpreted as v1 and restore incorrect state instead of failing clearly.

Proposed fix
	restore(snapshot: MessageTreeSnapshot<TMessage>) {
+		if (snapshot.version !== 1) {
+			throw new Error(`Unsupported message tree snapshot version ${snapshot.version}`);
+		}
		const restored = new MessageTree<TMessage>();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/thread/src/message-tree.ts` around lines 234 - 242, Update
MessageTree.restore to validate snapshot.version before iterating snapshot.nodes
or applying snapshot.cursorId. Accept the currently supported version 1 and
throw a clear error for any unsupported or future version, preventing
incompatible snapshots from being restored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/thread/src/message-tree.ts`:
- Around line 44-47: Update getParent and the related getPath logic to check
parent/message IDs explicitly for null or undefined rather than by truthiness,
so an empty-string ID is preserved and resolved like any other valid string ID.
Add a test covering an empty-ID message through both parent lookup and path
traversal.
- Around line 234-242: Update MessageTree.restore to validate snapshot.version
before iterating snapshot.nodes or applying snapshot.cursorId. Accept the
currently supported version 1 and throw a clear error for any unsupported or
future version, preventing incompatible snapshots from being restored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 376972f2-cf1d-40d5-8f35-bfc473e84ac4

📥 Commits

Reviewing files that changed from the base of the PR and between ac50cdf and 0a49a65.

📒 Files selected for processing (2)
  • packages/thread/src/message-tree.ts
  • packages/thread/test/message-tree.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/thread/test/message-tree.test.ts

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