Concepts · Durable state

Conversations and turns

A Heddle conversation is durable product state, not a prompt array that the host rebuilds for every request. The conversation engine owns how sessions and turns evolve; your product owns who may access them and where production records live.

Customization depth

Depth 4 — own the conversation lifecycle and presentation

Hosting depth

In-process Node.js engine; the same semantics sit beneath hosted runs

Documentation status

Canonical reference

Assumptions

  • You are using a Node.js 20+ TypeScript host.
  • Your product can assign or persist a stable Heddle session ID for each product conversation.
  • You have selected a durable stateRoot or injected a ChatSessionRepository.
Heddle owns
  • Session records, messages, turn summaries, queued prompts, continuation state, compaction metadata, and leases.
  • Turn preflight, model and tool execution, approval callbacks, trace persistence, artifacts, and final turn summaries.
  • Optimistic session revisions and the rules for creating, updating, compacting, and continuing a session.
Your product owns
  • Authentication, tenant and user mapping, authorization, and the product conversation-to-session association.
  • Model and credential selection, product tools, system context, UI state, and product-specific result presentation.
  • Production repository implementation, retention, encryption, backup, deletion policy, and data residency.

The three levels of state

It helps to keep three related concepts separate:

ConceptLifetimeWhat it represents
Conversation engineNormally the lifetime of a host process or composition rootA configured runtime boundary with session, turn, and artifact services.
SessionDurable across processes and requestsOne persisted multi-turn conversation, including history, messages, turns, settings, queue, compaction state, and lease state.
TurnOne agent execution inside a sessionA prompt or continuation that runs the model/tool loop and persists a structured result back into the session.

The engine is created from product configuration. It does not represent one user or one chat. A single engine can work with many sessions that share the same workspace, state root, model defaults, tools, and repository adapters.

TypeScript
import { createConversationEngine } from '@roackb2/heddle'

const engine = createConversationEngine({
  workspaceRoot: process.cwd(),
  stateRoot: '/var/lib/my-product/agent-state',
  model: 'gpt-5.4',
  reasoningEffort: 'medium',
})

The returned engine has three public services:

  • engine.sessions owns persisted session reads and lifecycle operations;
  • engine.turns submits or continues agent turns;
  • engine.artifacts lists and reads artifacts through the engine's resolved artifact repository.

Do not reconstruct artifact paths or bypass these services. Engine configuration can replace the default storage roots and repositories.

Session lifecycle

Create a reusable session before submitting the first turn:

TypeScript
const session = await engine.sessions.create({
  name: 'Support investigation',
  model: 'gpt-5.4',
  reasoningEffort: 'medium',
})

Use createOneOff when the product intentionally wants one-off retention semantics. The session service also supports listing, catalog pagination, read, require, latest, rename, pin, archive, settings updates, reset, and delete.

There are two subtly different persisted reads:

  • readExisting(id) returns only a stored session and never materializes Heddle's host-facing fallback session;
  • read(id) follows the normal host-facing behavior and can include that fallback in an otherwise empty repository.

Use readExisting for API existence checks and authorization-sensitive lookups. Use require when absence is exceptional and should fail the operation.

Your product should persist the returned Heddle session ID beside its own conversation record. Heddle deliberately does not add tenant, account, membership, or row-level-security fields to ChatSession. Scope the repository with trusted server-side identity instead of trusting a tenant identifier supplied by a browser.

Submit a new turn

Use engine.turns.submit(...) for a new user instruction in an existing session:

TypeScript
const result = await engine.turns.submit({
  sessionId: session.id,
  prompt: 'Summarize the incident and propose the next safe action.',
  host: {
    events: {
      onActivity(activity) {
        renderActivity(activity)
      },
    },
    approvals: {
      requestToolApproval(request) {
        return decideWithProductPolicy(request)
      },
    },
  },
})

A turn performs more than one model call. Heddle prepares the persisted history, acquires the session lease, compacts when needed, resolves the selected model and credentials, composes tools and host extensions, runs the model/tool loop, records trace evidence, persists the result, and releases the lease.

The returned turn result is the stable host-facing summary:

TypeScript
result.outcome
result.summary
result.failure
result.session
result.traceFile
result.artifacts
result.toolResults

Use failure.code for product behavior. It safely distinguishes model failures such as authentication, permission, quota, rate_limit, request, transport, empty_response, and unknown. Do not parse provider error text or the human-readable summary.

Continue an interrupted or prior run

Use engine.turns.continue(...) for explicit continuation semantics:

TypeScript
const result = await engine.turns.continue({
  sessionId: session.id,
  prompt: 'Continue from the saved state and finish the verification.',
  host,
})

If a prompt is supplied, Heddle submits it against the same durable session. If it is omitted, Heddle reuses the stored last continuation prompt when the session has prior history. Continuing an empty session without a remembered continuation fails instead of inventing work.

Do not implement continuation by replaying a product transcript into a brand-new session. That creates two histories, bypasses Heddle's compaction and turn records, and makes artifacts and traces harder to associate with the original conversation.

Conversation history and presentation

A persisted ChatSession contains different views for different responsibilities:

  • history is the model-facing message history used by runtime execution;
  • messages is the user/assistant conversation presentation state;
  • turns contains durable turn summaries and evidence links;
  • queuedPrompts stores accepted prompts waiting for execution;
  • context and archives hold token estimates and compaction state;
  • lease coordinates active ownership of a session;
  • model and reasoning settings can be stored per session.

Treat this record as Heddle-owned schema. A custom repository persists the complete record and revisions; it should not independently rewrite message, lease, queue, or compaction semantics.

Compaction preserves the conversation contract

Long conversations eventually exceed a practical model context. Heddle can compact older history and persist the resulting archive and summary metadata while keeping the session ID and product relationship stable.

The host may observe compaction through host.compaction callbacks, but should not race Heddle with a second summarization system. If the product wants a user-facing conversation summary, derive it as presentation data; do not replace the engine's model-facing history behind its back.

Leases prevent competing turn owners

Session leases record which runtime currently owns a turn. Turn execution acquires and releases the lease as part of the engine lifecycle. This prevents two hosts from silently mutating the same conversation at once.

If a host crashes and leaves an operationally stale lease, use the explicit lease APIs and owner identity to inspect or clear it. Do not delete lease fields directly in storage. Custom repositories must preserve optimistic revisions so two processes cannot overwrite one another's session changes.

Default and custom persistence

With no repository supplied, Heddle uses the file-backed session adapter beneath stateRoot. This is suitable for a local, one-machine product and uses immutable revision bodies plus atomic catalog replacement.

Hosted products can inject an async ChatSessionRepository:

TypeScript
const engine = createConversationEngine({
  workspaceRoot,
  stateRoot,
  model,
  sessionRepository: {
    list: (input) => listAuthorizedSessions(input),
    read: (sessionId) => readAuthorizedSession(sessionId),
    create: (stored) => createAuthorizedSession(stored),
    update: (input) => compareAndSwapAuthorizedSession(input),
    delete: (input) => compareAndSwapDelete(input),
  },
})

Repository updates and deletes use expected revisions. A production adapter must distinguish missing records, already-existing records, and revision conflicts rather than applying last-write-wins behavior.

Session and artifact repositories are injectable today. Traces and memory remain path-oriented beneath stateRoot, so a hosted deployment must still choose a durable and appropriately isolated state root for those domains.

Invariants to verify

Before shipping a conversation integration, verify all of the following:

  1. A second turn uses the same Heddle session ID and sees the first turn's persisted history.
  2. An unauthorized user cannot read, mutate, continue, or delete another scope's session.
  3. Concurrent writers produce a revision conflict instead of losing a newer update.
  4. A process restart can reopen a session with its messages, turns, queue, lease, and compaction state intact.
  5. Approval denial and model failures persist an honest terminal result rather than appearing successful.
  6. Product UI is derived from activity and result contracts without becoming a second source of conversation truth.

Canonical sources