LangGraph State Channels Deep Dive
Understanding channels and reducers is required before refactoring the pain discovery gate. This day grounds the mergeIntoState pattern from PR #81 in LangGraph primitives so the refactor is reliable rather than ad-hoc.
Resources
- 25 min
- 15 min
Codebase anchors
The Tribunal code that demonstrates today's concept. Click the line to open in GitHub or VS Code.
This mergeIntoState function is the existing manual state handling pattern that LangGraph channels and reducers will replace on this day.
116 * returns an object via spread (`{ ...state, x: 1 }`), every state117 * field is a candidate for the reducer. The reducer118 * `(left, right) => right ?? left` works correctly for declared119 * channels — but the spread can carry over fields whose values came120 * from a stale state-snapshot reference, depending on how LangGraph's121 * internals choose to deliver state to the next node.122 *123 * This shape was the suspected root cause of the 2026-05-30 Cuba/NA124 * leak: webSearchAttempted was set to true by webSearchFallbackNode,125 * but the second-pass selectSignalNode read it as undefined — even126 * though declaring it as a channel should have preserved it.127 *128 * Explicit field-by-field returns give the reducer a clear write per129 * channel and don't rely on JavaScript spread semantics being130 * compatible with LangGraph's snapshot model.131 *132 * Every node now goes through this helper. If a future field is133 * added to PainDiscoveryState, this function fails the TypeScript134 * exhaustiveness check until the field is wired here too.135 */136function mergeIntoState(137 state: PainDiscoveryState,138 deltas: Partial<PainDiscoveryState>,139): PainDiscoveryState {140 return {141 factors: deltas.factors ?? state.factors,142 traceId: deltas.traceId ?? state.traceId,143 painSignals: deltas.painSignals ?? state.painSignals,144 selectedSignal:145 deltas.selectedSignal !== undefined ? deltas.selectedSignal : state.selectedSignal,146 searchAttempts: deltas.searchAttempts ?? state.searchAttempts,147 searchStrategies: deltas.searchStrategies ?? state.searchStrategies,148 painConfidence: deltas.painConfidence ?? state.painConfidence,149 usedSynthetic: deltas.usedSynthetic ?? state.usedSynthetic,150 syntheticRate: deltas.syntheticRate ?? state.syntheticRate,151 needsCountryRelevantFallback:152 deltas.needsCountryRelevantFallback !== undefined153 ? deltas.needsCountryRelevantFallback154 : state.needsCountryRelevantFallback,155 webSearchAttempted:156 deltas.webSearchAttempted !== undefinedThis call site shows current delta merging that must be expressed via typed channels after the deep dive.
217 searchAfricanSites(factors as any),218 ]);219 return [...registryRss, ...hnSignals, ...ihSignals, ...phSignals, ...africanSignals];220}221 222// ---------------------------------------------------------------------------223// Nodes224// ---------------------------------------------------------------------------225 226/** Primary search — exact factors, all sources in parallel. */227export async function searchPrimaryNode(228 state: PainDiscoveryState,229): Promise<PainDiscoveryState> {230 console.log('[PAIN_DISCOVERY] Primary search with exact factors:', state.factors);231 232 try {233 const allSignals = await fanOutAllSources(state.factors);234 const rankedSignals = rankPainSignals(allSignals, state.factors as any);235 console.log(`[PAIN_DISCOVERY] Primary search found ${rankedSignals.length} signals`);236 237 return mergeIntoState(state, {238 painSignals: rankedSignals,239 searchAttempts: 1,240 searchStrategies: ['primary'],241 });242 } catch (error) {243 console.error('[PAIN_DISCOVERY] Primary search error:', error);244 return mergeIntoState(state, {245 painSignals: [],246 searchAttempts: 1,247 searchStrategies: ['primary'],248 });249 }250}251 252/**253 * Adjacent search — same sources, broadened factors.254 * Attempt 1: broaden region to regional cluster.255 * Attempt 2: drop demographic constraint.256 * Attempt 3: both.257 */Deliverable
Journal entry with channel schema and reducer plan for the PR #81 refactor
Quiz · 2 questions
1. What happens when two nodes write to the same channel without a reducer?
2. Write the reducer signature needed to merge painSignals arrays across nodes.