DocumentsLobeHub
Conversation Message Processing
Conversation Message Processing
Type
Topic
Status
Published
Created
Jul 7, 2026
Updated
Jul 7, 2026

Conversation Message Processing#

Two packages collaborate to turn raw hierarchical message data into AI-model-compatible output:

  • conversation-flow — parses a flat message array into structured display representations (contextTree and flatList). Its public API is a single parse() function.
  • context-engine — runs a configurable pipeline of processors over a PipelineContext to transform those messages before they are sent to an AI model. Orchestrated by the ContextEngine class.

The two packages serve complementary roles: conversation-flow builds the display model from raw DB messages; context-engine prepares those messages for the model API before each inference call.

conversation-flow: Three-Phase Parse Pipeline#

parse(messages, messageGroups?) is the single entry point . It runs three phases:

  1. IndexingbuildHelperMaps builds a messageMap and childrenMap for O(1) access patterns.
  2. StructuringbuildIdTree converts flat parent-child relationships into a tree of IdNodes.
  3. Transformation — a Transformer instance produces two outputs :
    • contextTree — semantic tree for navigation (node types: MessageNode, AssistantGroupNode, BranchNode, SignalCallbacksNode, etc.)
    • flatList — flat array optimised for virtual list rendering

A pre-processing step before Phase 1 remaps sub_agent-scoped messages to use their subAgentId as agentId, so downstream collectors never merge messages from different agents into the same group .

Post-processing promotes metadata.usage to a top-level usage field on each flat message, normalising token data written by different executor paths .

Dual-Form Message Chain Support#

The parser handles two persisted parentage shapes that must produce identical output :

FormNext assistant's parentId
tool-anchored (old)Last tool result message ID
assistant-anchored (new)Previous assistant message ID (sibling of its tool results)

Both forms, and any mix within a single turn, collapse to the same assistantGroup node in flatList . Key invariants enforced by the reader:

  1. A tool message is always inline data of its assistant (both forms).
  2. A branch is ≥ 2 non-tool siblings under one parent.

The test suite dualForm.test.ts documents seven fixture classes: old form, new form, mixed forms, parallel tools, regenerate-branch, assistant-anchored regenerated continuation, and async-task summary placement.

MessageCollector#

MessageCollector is the workhorse of the transformation phase. It is constructed with a messageMap, childrenMap, and a BranchResolver. Key methods:

MethodPurpose
collectToolMessages(assistant, messages)Resolves tool results for an assistant. Prefers the explicit tool.result_msg_id; falls back to matching by parentId + tool_call_id.
isToolChainHead(assistant)Returns true when a toolless assistant is the head of a chain that eventually reaches a tool-using step — prevents that head from rendering as a standalone bubble.
collectAssistantChain(...)Recursively walks assistant → tools → assistant → …, staying within the same agentId. Handles toolless continuation steps (prose-only progress updates) without breaking the visual chain.
findFlatChainContinuation(...)Dual-form aware: gathers candidates from both the assistant's own non-tool children (new form) and each tool result's children (old form). Applies a fan-out guard (AgentCouncil or task children end the chain) and branch resolution for regenerated continuations.
collectFlatSignalCallbacks(...)Collects tool-stdout / tool-callback signal blocks, sorted by metadata.signal.sequence.
collectFlatTaskCompletions(...)Collects task-completion signal assistants (post-task summaries), sorted by createdAt.

The signal lineage (metadata.signal) is read by getMessageSignal, which deliberately returns undefined when the assistant carries tools — the writer attaches the tag at stream start before knowing whether tools will be used, so the collector defangs that mismatch.

Phase 2 migration note: When a dedicated messages.signal column lands, swap metadata?.signal for (msg as any).signal ?? msg.metadata?.signal in getMessageSignal .

BranchResolver#

BranchResolver determines which branch is active when multiple non-tool siblings exist under one parent (e.g. a regenerated reply). It uses a three-tier priority :

  1. metadata.activeBranchIndex — explicit integer index; if index === children.length, the branch is being optimistically created and undefined is returned.
  2. Infer from descendants — the child that already has descendants is the active one.
  3. Default — first child.

Two method variants exist for different traversal contexts :

  • getActiveBranchId(message, idNode) — used during contextTree building (works on IdNode structures).
  • getActiveBranchIdFromMetadata(message, childIds, childrenMap) — used during flat-list building.

MessageCollector.resolveActiveContinuationId() calls getActiveBranchIdFromMetadata to pick the correct continuation when >1 non-tool same-agent siblings share a parent .

context-engine: ContextEngine Pipeline#

ContextEngine sequences an ordered array of ContextProcessor instances over a mutable PipelineContext :

{ messages, metadata, initialState, isAborted } → Processor₁ → Processor₂ → … → PipelineResult
  • Early termination: any processor can set context.isAborted = true; subsequent processors are skipped .
  • Error wrapping: processor failures are caught and re-thrown as PipelineError with the processor name and a 300-char cause summary for dashboard triage .
  • Stats: PipelineResult includes per-processor processorDurations and totalDuration .
  • Fluent API: addProcessor, removeProcessor, clear, clone, validate .

Key Processors#

All processors extend BaseProcessor (abstract class implementing the ContextProcessor interface) and are exported from packages/context-engine/src/processors/index.ts.

Message Structure#

ProcessorWhat it does
GroupMessageFlattenProcessorExplodes role=assistantGroup / role=supervisor messages with a children array back into flat assistant + tool sequences that AI model APIs understand . Skips empty-children groups entirely .
AgentCouncilFlattenProcessorHandles AgentCouncil fan-out message structures.
GroupRoleTransformProcessorTransforms roles for group messages before API calls.
SupervisorRoleRestoreProcessorRestores supervisor role back to assistant before the model API call (mirrors the isSupervisor transform applied in parse()).

Tool Handling#

ProcessorWhat it does
ToolCallProcessorNormalises tool call messages.
ToolMessageReorderEnsures tool result messages appear in model-expected order.
DisabledToolCallFilterRemoves tool calls that are disabled.

Async Task Support#

ProcessorWhat it does
TasksFlattenProcessorFlattens async task trees.
TaskMessageProcessorTransforms task messages.
TaskCallbackMessageProcessorHandles task callback/signal messages.

Context Management#

ProcessorWhat it does
HistoryTruncateProcessorEnforces message window limits.
MessageCleanupProcessorRemoves stale or invalid messages.
MessageContentProcessorNormalises message content formats.
PlaceholderVariablesProcessorSubstitutes template variables.
InputTemplateProcessorApplies input templates.
VerifyMessageProcessorValidates message integrity before dispatch.
Conversation Message Processing | Dosu