FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
yawnxyz

yawnxyz

chatter

Public
Like
chatter
Home
Code
15
backend
4
frontend
18
scripts
2
.gitignore
.vtignore
BRANCH_DATA_MODEL.md
chatCompletion.js
chatStreaming.js
deno.json
deno.lock
index.html
jsonUtils.js
H
main.js
notes.md
readme.md
Branches
1
Pull requests
Remixes
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
BRANCH_DATA_MODEL.md
Code
/
BRANCH_DATA_MODEL.md
Search
…
Viewing readonly version of main branch: v194
View latest version
BRANCH_DATA_MODEL.md

Branching Data Model Plan

Goals

  • Support multiple branching responses per conversation while remaining backward compatible with the current linear model.
  • Minimize disruption to UI logic, Dexie storage, and backend streaming/jobs.
  • Avoid data duplication and keep branch operations efficient and safe (low risk of corruption).
  • Keep export/import simple and resilient.

Non‑Goals

  • No server-side persistence changes right now (branches live in Dexie, as today).
  • No immediate large refactors of streaming/job code; aim for additive changes that do not break current flows.

Current State (v10)

  • Dexie tables:
    • chats: root entities for conversations.
    • messages: linear log of { id, chatId, createdAt, role, content, reasoning?, model?, promptLabel? }.
    • folders: sidebar organization.
    • systemPrompts: preset library (may link to chats; per‑chat settings override possible).
    • notes: global notes plugin.
  • UI maintains a single linear messages array per chat. Reruns/regenerations mutate the tail of the list (truncate then stream new assistant message).
  • Backend is stateless: /api/jobs streams assistant deltas; the client persists to Dexie.

Limitations: no notion of branching. Regenerations overwrite the tail; alternatives cannot coexist.


Requirements for Branching

  • Allow multiple alternative assistant replies to a prior point in the conversation (fork at any message).
  • Allow users to name/select branches and continue a branch independently.
  • Present a linear view of the currently selected path so most UI remains unchanged.
  • Backward compatible: existing chats remain usable without any branching changes.
  • Keep performance acceptable for long conversations.
  • Preserve streaming behavior, message editing, deletion, export/import.

Design Options Considered

Option A: Add branchId on messages + branches table

  • Messages carry a branchId. Forking creates a new branch ID and subsequent messages adopt it.
  • Pros: simple grouping/querying; easy to filter by branch.
  • Cons: shared pre‑branch messages either need duplication (bad) or special handling across branches; renames/moves can get tricky; handling of edits to shared ancestors is complex.

Option B: Parent‑pointer tree (recommended) + named branch views

  • Each message gets a parentId to form a tree per chat. A branch is a named pointer to a leaf message (leafMessageId). The current path is built by walking parentId from leaf to root and reversing.
  • Pros: no duplication of pre‑branch content; natural for forking/regenerating; easy to compute linear view per selected leaf; migrations are straightforward (set parent to previous message for legacy linear threads).
  • Cons: requires path building for display and for request payloads; ancestor edits impact all descendants unless we adopt copy‑on‑write rules.

Option C: Collapse everything into a single sessions document (embedded tree)

  • Store the whole chat tree as one JSON blob.
  • Pros: conceptually unified; single read/write.
  • Cons: high risk of corruption, hard to do partial updates/streams, large document rewrites, difficult conflict handling, and complicated migrations. Not recommended.

Recommendation: Option B (parent‑pointer tree with named branch views).


Proposed Schema (Dexie v11) - Fixed Version

messages (updated)

  • Store signature with improved indexing:
    • ++id, [chatId+parentId], [chatId+deleted], createdAt, role, content, reasoning, model
  • New fields:
    • parentId: number | null — points to the immediate previous message on the selected path; null for root.
    • pathCache?: string — JSON array of message IDs from root to this message (performance optimization).
    • depth?: number — 0‑based depth from root for sorting/UX and cycle detection.
    • turnId?: string — grouping id for user/assistant turn when generating multiple alternatives.
    • deleted?: boolean — soft‑delete to avoid cascading hard deletes.
    • isShared?: boolean — cache flag: true if this message has multiple child branches (blocks editing).
  • Backward compat: if parentId is null/undefined for all messages in a chat, assume the legacy linear order by createdAt and treat each message's parent as the previous message when building views.
  • Integrity constraints:
    • parentId cannot equal id (prevent self-reference)
    • parentId must exist in same chatId if not null
    • depth must be parent.depth + 1 (cycle prevention)

branches (new)

  • Purpose: named pointers to leaves in the message tree (for selection/switching UI).
  • Store signature:
    • ++id, [chatId+name], leafMessageId, createdAt
  • Fields:
    • chatId: number
    • leafMessageId: number — the tip of a branch/path.
    • name: string — e.g., "main", "alt‑1", or user‑provided name.
    • baseMessageId?: number — where this branch diverged (informational; helps UI).
    • createdAt: number
    • updatedAt?: number
    • archived?: boolean
  • Integrity constraints:
    • name must be unique per chatId
    • leafMessageId must exist and belong to same chatId

chats (add safety fields)

  • Add activeLeafMessageId?: number — last selected branch tip for this chat.
  • Add migrationStatus?: 'pending' | 'complete' | 'failed' — prevents concurrent migrations.
  • Add migrationVersion?: number — tracks which migration was applied.
  • Add branchingEnabled?: boolean — feature flag per chat for gradual rollout.
  • No other breaking changes to existing fields.

Type sketch (for reference only; not code):

Create val
type Message = { id: number; chatId: number; parentId: number | null; createdAt: number; role: 'user' | 'assistant' | 'system'; content: string; reasoning?: string; model?: string | null; promptLabel?: string | null; rootId?: number; depth?: number; turnId?: string; deleted?: boolean; }; type Branch = { id: number; chatId: number; leafMessageId: number; name: string; baseMessageId?: number; parentBranchId?: number | null; createdAt: number; updatedAt?: number; archived?: boolean; };

Indexing guidance:

  • messages: chatId, parentId, createdAt indices support path building and children lookups.
  • branches: chatId index for listing branches per chat.

Migration Plan (to v11) - Safe Version

Pre-Migration Safety Checks

  1. Lock Migration: Check for migrationStatus = 'pending' in any chat. If found, wait or abort to prevent concurrent migrations.
  2. Backup Validation: Ensure export functionality works before starting migration.

Migration Steps

  1. Schema Update: Add Dexie version 11 with new fields. Use Dexie transactions for all operations.

  2. Chat-by-Chat Migration (atomic per chat):

    Create val
    await db.transaction('rw', db.chats, db.messages, async () => { // Mark migration start await db.chats.update(chatId, { migrationStatus: 'pending', migrationVersion: 11 }); // Load messages in createdAt order const messages = await db.messages.where('chatId').equals(chatId).sortBy('createdAt'); // Set parentId chain for (let i = 0; i < messages.length; i++) { const parentId = i === 0 ? null : messages[i-1].id; const depth = i; const pathCache = JSON.stringify(messages.slice(0, i+1).map(m => m.id)); await db.messages.update(messages[i].id, { parentId, depth, pathCache, isShared: false // will be computed later }); } // Mark migration complete await db.chats.update(chatId, { migrationStatus: 'complete' }); });
  3. Compute Shared Flags: After all chats migrated, compute isShared flags by counting children.

  4. Validation: Verify each chat's message chain is valid (no cycles, proper depths).

  5. Rollback on Failure: If any step fails, mark migrationStatus: 'failed' and provide recovery tools.

Migration Recovery

  • Failed Migration Detection: On app start, check for migrationStatus: 'failed' chats.
  • Automatic Retry: Attempt re-migration with exponential backoff.
  • Manual Recovery: Provide UI to export failed chat data and reimport after manual fix.

UI Changes (incremental) - Safe Version

New UI state

  • currentLeafMessageId: number | null — determines which path is shown.
  • branchList: Branch[] — list of named branch tips for the current chat.
  • streamingMessageId: number | null — ID of message currently being streamed to (prevents conflicts).
  • cachedMessagePath: Message[] — cached linear view to avoid recomputation.
  • branchingEnabled: boolean — per-chat feature flag.

Safe Path Building

Create val
// Use pathCache when available, fall back to traversal function buildMessagePath(leafMessageId: number): Message[] { const leaf = messages.find(m => m.id === leafMessageId); if (!leaf) return []; // Try cached path first if (leaf.pathCache) { try { const ids = JSON.parse(leaf.pathCache); const path = ids.map(id => messages.find(m => m.id === id)).filter(Boolean); if (path.length === ids.length) return path; // cache hit } catch {} } // Fallback: traverse parents (with cycle detection) const path = []; let current = leaf; const visited = new Set(); while (current && !visited.has(current.id)) { visited.add(current.id); path.unshift(current); current = current.parentId ? messages.find(m => m.id === current.parentId) : null; } return path; }

Branching Actions (Safe)

  • Fork at message:

    1. Check if branchingEnabled for chat
    2. Validate that base message exists and user has permission
    3. Generate unique branch name (check for conflicts)
    4. Use transaction to create branch + update activeLeafMessageId
  • Regenerate alternative:

    1. Check if parent message is not currently being streamed to
    2. Create new assistant message with proper parentId
    3. Update parent's isShared flag if needed
    4. Set streamingMessageId to prevent conflicts
  • Switch branches:

    1. Cancel any active streaming to old branch
    2. Update currentLeafMessageId and rebuild cached path
    3. Update chat's activeLeafMessageId in DB

Safe Branch Deletion

Create val
async function deleteBranch(branchId: number) { const branch = await db.branches.get(branchId); if (!branch) return; // Check for orphaned messages const wouldOrphan = await checkForOrphans(branch.leafMessageId); if (wouldOrphan.length > 0) { // Option 1: Block deletion throw new Error(`Cannot delete branch: ${wouldOrphan.length} messages would become orphaned`); // Option 2: Auto-create recovery branch // await createRecoveryBranch(wouldOrphan); } await db.branches.update(branchId, { archived: true }); }

Editing & Deleting Safety

  • Block editing shared ancestors: If message.isShared === true, show "Cannot edit shared message" error.
  • Soft delete only: Never hard-delete messages, only set deleted: true.
  • Validate permissions: Check if user can edit (not shared, not currently streaming).
  • Preserve integrity: After any edit, recompute isShared flags for affected messages.

Streaming Conflict Prevention

Create val
// Before starting stream if (this.streamingMessageId && this.streamingMessageId !== assistantMessageId) { throw new Error('Another message is currently being generated'); } this.streamingMessageId = assistantMessageId; // In applyJobDelta if (ctx.assistantMessageId !== this.streamingMessageId) { console.warn('Stream target mismatch, ignoring delta'); return; // Ignore stale deltas } // After stream completion this.streamingMessageId = null;

Backend & Jobs Impact - Safe Version

Validation at Job Start

Create val
// In jobs.js - before creating assistant placeholder const payload = { // ... existing fields expectedLeafMessageId: this.currentLeafMessageId, // Add validation context branchingEnabled: this.branchingEnabled }; // Client validates before starting job if (this.streamingMessageId) { throw new Error('Another message is being generated'); }

Safe Message Payload Construction

Create val
// Build messages from current branch path only const messages = buildMessagePath(this.currentLeafMessageId); const chatMessages = messages.map(m => ({ role: m.role, content: m.content })); // Add system prompt at the beginning (not part of tree structure) const systemPrompt = getCurrentSystemPrompt(); const finalMessages = systemPrompt ? [{ role: 'system', content: systemPrompt }, ...chatMessages] : chatMessages;

Delta Application with Conflict Detection

Create val
// In applyJobDelta - add safety checks this.applyJobDelta = async (jobId, parsed) => { const ctx = (this._jobCtx || {})[jobId]; if (!ctx) return; // Validate we're still on the expected branch if (this.currentLeafMessageId !== ctx.expectedLeafMessageId) { console.warn('Branch switched during streaming, cancelling job'); this.jobManager.cancelJob(jobId); return; } // Validate the target message still exists and is streamable const targetMessage = await this.db.messages.get(ctx.assistantMessageId); if (!targetMessage || targetMessage.deleted) { console.warn('Target message deleted during stream'); return; } // Apply delta as before... const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : null; const delta = choice?.delta || {}; if (delta.content && ctx.chatId === this.currentChatId) { // ... existing content application logic } };

Job Context Enhancement

Create val
// Enhanced job context for safety this._jobCtx[jobId] = { chatId: this.currentChatId, assistantMessageId, assistantCreatedAt, expectedLeafMessageId: this.currentLeafMessageId, // Track expected branch branchPath: this.cachedMessagePath.map(m => m.id), // Snapshot for validation startTime: Date.now() };

Export / Import

  • Extend export to include: messages.parentId, rootId, depth, turnId, deleted, and the branches table.
  • On import, if parentId is missing, reconstruct the linear chain; branches can be omitted or reconstructed later.

Data Integrity Rules & Warnings

  • Prevent cycles: enforce at write time that parentId cannot create a loop.
  • Copy‑on‑write for ancestor edits to avoid cross‑branch mutations.
  • Deletion policy: prefer soft‑delete; if hard delete is requested on a node with children, either block or cascade with explicit user confirmation.
  • Multi‑tab concurrency: use Dexie transactions for branch creation and for streaming writes to avoid races (especially when concurrently generating alternatives).
  • Depth/cache fields (rootId, depth) are hints. If missing or stale, recompute via parent traversal; never rely solely on them.

Performance Considerations

  • Path building is O(depth). Typical chats are shallow; acceptable. Cache the computed path in memory and update incrementally as streaming appends new nodes.
  • Index by parentId for quick children lookups (e.g., to list alternatives under the same parent/user turn).
  • Avoid duplicating shared ancestors; the parent‑pointer design keeps storage small and writes minimal.

Rollout Plan

  1. Ship Dexie v11 schema (no UI behavior change yet). Migrate parentId linearly.
  2. Add hidden path builder and ensure existing flows still work when linear.
  3. Introduce branch creation/switching UI behind a feature flag.
  4. Update sendMessage/rerun* to use parentId when the flag is enabled.
  5. Add branch management UI (rename, delete/archive) and export/import extensions.
  6. Remove feature flag after validation.

Testing Checklist

  • Migration:
    • Old data loads; linear view identical to pre‑migration.
    • Export/import round‑trip with and without parentId.
  • Branching:
    • Fork at any message; both branches are navigable.
    • Regenerate alternative responses; multiple assistant siblings appear under a user message.
    • Streaming writes go to the correct placeholder.
    • Deleting/Editing ancestors behaves per policy (copy‑on‑write or blocked).
  • Performance:
    • Large chats path compute remains responsive.
    • No race conditions under concurrent streams in separate branches.

On Collapsing Into a Single "Session" Item

  • Not recommended. While it simplifies the conceptual model, it complicates partial updates, streaming, and recovery. It also increases the blast radius of corruption and makes concurrency and diffing difficult. The parent‑pointer design offers a good balance of simplicity, safety, and flexibility while keeping backward compatibility.

Critical Issues & Fixes Identified

🚨 Major Issues

1. Orphaned Messages After Branch Deletion

Problem: If a user deletes a branch that was the only path to certain messages, those messages become orphaned and unreachable. Fix: Before deleting a branch, check if any messages would become orphaned. Either:

  • Block deletion if orphans would be created
  • Auto-create a new branch pointing to orphaned subtrees
  • Add a "Find Orphaned Messages" recovery tool

2. Migration Race Conditions

Problem: If multiple tabs are open during v10→v11 migration, they could interfere with each other. Fix: Use Dexie transaction locks and add a migration status flag to prevent concurrent migrations.

3. Streaming to Wrong Assistant Placeholder

Problem: If user switches branches while assistant is streaming, deltas could write to the wrong message. Fix: Include expectedLeafMessageId in job context and validate on each delta. Cancel stream if mismatch detected.

4. Copy-on-Write Complexity

Problem: Editing shared ancestors with copy-on-write creates complex reattachment logic that could break parent chains. Fix: Initially, block editing of shared ancestors entirely. Add copy-on-write later as a separate feature with extensive testing.

⚠️ Medium Issues

5. Performance with Deep Conversations

Problem: Path building becomes O(depth) which could be slow for very long conversations. Fix: Add pathCache field to store pre-computed paths, invalidated on parent changes.

6. Branch Name Conflicts

Problem: User could create multiple branches with the same name. Fix: Enforce unique branch names per chat in Dexie schema constraints.

7. Inconsistent State After Partial Failures

Problem: If creating a branch partially fails (branch created but message creation fails), state becomes inconsistent. Fix: Use Dexie transactions for all branch operations to ensure atomicity.

🔧 Minor Issues

8. Missing Indexes for Performance

Problem: Current index plan may not cover all query patterns efficiently. Fix: Add compound index [chatId, parentId] and [chatId, deleted] for faster queries.

9. No Cycle Detection Implementation Details

Problem: Plan mentions preventing cycles but doesn't specify how. Fix: Implement depth-limited parent traversal during writes to detect cycles.

10. System Messages Placement

Problem: System messages (like preset instructions) need special handling in branched conversations. Fix: Always place system messages at the absolute root with no parent, before any user messages.


Revised Schema Fixes

Updated messages store signature:

++id, [chatId+parentId], [chatId+deleted], createdAt, role, content, reasoning, model

Add integrity constraints:

  • parentId cannot equal id (self-reference)
  • parentId must exist in same chatId if not null
  • Branch names must be unique per chatId

Add migration safety:

Create val
// Add to chats table migrationStatus?: 'pending' | 'complete' | 'failed' migrationVersion?: number

Summary

  • Adopt a parent‑pointer message tree per chat and represent branches as named pointers to leaf messages.
  • Migrate legacy linear chats by assigning parentId to previous message.
  • Keep backend/jobs unchanged; client builds the path for requests and display.
  • Provide strong integrity and UX policies for editing/deleting ancestors and for managing branches.
  • Address critical streaming, orphaning, and migration safety issues before implementation.
FeaturesVersion controlCode intelligenceCLI
Use cases
TeamsAI agentsSlackGTM
ExploreDocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareersBrandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.