CausalityService
Layer 1 service that tracks decision causality and enables reasoning reconstruction.
Overview
CausalityService is the implementation of Layer 1 (Causality Engine). It tracks WHY contexts were created by maintaining causal relationships and enabling backwards traversal through decision history.
Location: src/domain/services/CausalityService.ts
Layer: Layer 1 - Past (WHY)
Purpose: Understand historical causality and decision chains
Constructor
constructor(private readonly repository: IContextRepository)Parameters:
repository: Database abstraction for context storage
Dependency Injection: Repository is injected, not created internally (testable, swappable)
Methods
buildCausalChain(snapshotId: string): Promise<CausalChainNode[]>
Trace decision history backwards from a context to its root cause.
Parameters:
snapshotId: Starting context ID to trace from
Returns: Array of CausalChainNode with root at index 0
Algorithm:
1. Start at target snapshot
2. Look up snapshot in repository
3. Check if it has causedBy parent
4. If yes, fetch parent and add to chain
5. Repeat until causedBy is null (root found)
6. Return chain with root firstExample:
const chain = await causalityService.buildCausalChain('decision-id');
// Returns:
// [
// { snapshot: {...}, depth: 0 }, // Root (conversation)
// { snapshot: {...}, depth: 1 }, // Research
// { snapshot: {...}, depth: 2 } // Decision
// ]Edge Cases:
- Circular references: Prevented by
visitedset - Broken chains: Stops at missing parent
- Single node: Returns array with one element
Performance: O(n) where n = chain length (typically 3-10)
reconstructReasoning(snapshotId: string): Promise<string>
Generate human-readable explanation of why a context was created.
Parameters:
snapshotId: Context to explain
Returns: Formatted markdown string
Example:
const reasoning = await causalityService.reconstructReasoning('abc-123');
// Returns:
// **Action Type**: decision
//
// **Rationale**: OAuth2 with PKCE is more secure than JWT
//
// **Context Summary**: Decision to use OAuth2 for mobile appFormat:
**Action Type**: {actionType}
**Rationale**: {rationale}
**Context Summary**: {summary}Edge Cases:
- No causality metadata: Returns summary only
- Missing rationale: Shows "No rationale provided"
getCausalityStats(project: string): Promise<CausalityStats>
Get aggregate statistics on causal relationships for a project.
Parameters:
project: Project to analyze
Returns: Statistics object
Return Type:
interface CausalityStats {
totalWithCausality: number;
averageChainLength: number;
actionTypeCounts: Record<ActionType, number>;
rootCauses: number;
}Example:
const stats = await causalityService.getCausalityStats('my-project');
// Returns:
// {
// totalWithCausality: 42,
// averageChainLength: 3.5,
// actionTypeCounts: {
// conversation: 10,
// research: 15,
// decision: 12,
// file_edit: 5,
// tool_use: 0
// },
// rootCauses: 10
// }Performance:
- Queries all contexts for project
- Samples 10 chains for average length calculation
- O(n) where n = context count
Types
CausalChainNode
interface CausalChainNode {
snapshot: IContextSnapshot;
causedBy: CausalChainNode | null;
children: CausalChainNode[];
depth: number; // Distance from root (0 = root)
}ActionType
type ActionType =
| 'conversation' // User dialogue
| 'decision' // Strategic choice
| 'file_edit' // Code modification
| 'tool_use' // External tool
| 'research'; // Information gatheringUsage Examples
Build and Display Chain
const chain = await causalityService.buildCausalChain('decision-id');
console.log(`Chain has ${chain.length} steps:`);
for (const node of chain) {
const indent = ' '.repeat(node.depth);
console.log(`${indent}- ${node.snapshot.causality?.actionType}: ${node.snapshot.summary}`);
}
// Output:
// Chain has 3 steps:
// - conversation: User asks about authentication
// - research: OAuth2 best practices research
// - decision: Use OAuth2 with PKCEFind Root Causes
const stats = await causalityService.getCausalityStats('my-project');
console.log(`Project has ${stats.rootCauses} root causes`);
// Root causes are entry points - user questions, external eventsExplain Decision
const reasoning = await causalityService.reconstructReasoning('decision-id');
console.log(reasoning);
// Shows:
// - What type of action created it
// - Why it was created (rationale)
// - What the context is about (summary)Best Practices
1. Always Set causedBy When Relevant
// ✅ Good: Links to parent
save_context({
content: 'Implementation...',
metadata: {
causedBy: decisionId,
actionType: 'file_edit'
}
});
// ❌ Bad: Orphaned context
save_context({
content: 'Implementation...'
});2. Use Appropriate Action Types
Choose the type that best describes what happened:
- User asked question →
conversation - You researched →
research - You made choice →
decision - You wrote code →
file_edit - You ran tool →
tool_use
3. Provide Clear Rationale
// ✅ Good: Clear reasoning
metadata: {
rationale: 'OAuth2 with PKCE prevents auth code interception on mobile'
}
// ❌ Bad: Vague
metadata: {
rationale: 'Better security'
}Testing
Unit Test Example
describe('CausalityService', () => {
it('builds 3-level causal chain', async () => {
const mockRepo = {
findById: jest.fn()
.mockResolvedValueOnce({
id: '3',
causality: { causedBy: '2' }
})
.mockResolvedValueOnce({
id: '2',
causality: { causedBy: '1' }
})
.mockResolvedValueOnce({
id: '1',
causality: { causedBy: null } // Root
})
};
const service = new CausalityService(mockRepo);
const chain = await service.buildCausalChain('3');
expect(chain).toHaveLength(3);
expect(chain[0].snapshot.id).toBe('1'); // Root first
expect(chain[2].snapshot.id).toBe('3'); // Target last
});
});Error Handling
Context Not Found
try {
const chain = await causalityService.buildCausalChain('invalid-id');
} catch (error) {
// Handle: Context doesn't exist or was pruned
}Circular Chain Detection
The algorithm prevents infinite loops:
const visited = new Set<string>();
while (currentId && !visited.has(currentId)) {
visited.add(currentId);
// Process...
}If a circular reference exists, the chain stops at the cycle point.
Integration with Other Layers
With Layer 2 (Memory)
Causal position affects memory importance:
- Root causes often stay in ACTIVE/RECENT tiers longer
- Leaf nodes may be archived faster
With Layer 3 (Propagation)
Causal position affects prediction score:
- Root causes get +0.3 score boost
- Decision nodes get +0.2 boost
- Branch points get +0.1 boost
See Also
- API Overview - Service architecture
- ContextSnapshot - Entity with causality metadata
- Layer 1: Causality Engine - Concepts and design
- build_causal_chain tool - MCP tool usage
- reconstruct_reasoning tool - MCP tool usage
