How DS2API Handles Server-Side Chat History Storage: Implementation Guide
DS2API implements server-side chat history storage as a resilient file-based JSON database with atomic writes, configurable retention limits, and RESTful admin endpoints, enabling real-time streaming updates and automatic error recovery.
The CJackHwang/ds2api repository provides a complete OpenAI-compatible API server with built-in chat history management. Understanding how DS2API handles server-side chat history storage reveals a lightweight yet robust architecture that balances persistence needs with performance requirements through atomic file operations and throttled incremental updates.
Architecture Overview
DS2API's chat history subsystem centers on three core components working together to provide durable storage without external database dependencies. According to the source code, the implementation spans across internal/chathistory/store.go for persistence logic, internal/httpapi/openai/chat/chat_history.go for session management, and internal/httpapi/admin/history/handler_chat_history.go for administrative control.
The system uses a dual-file structure: a JSON index file (chat_history.json) maintains lightweight summaries and metadata, while a companion detail directory (chat_history.json.d/) stores full conversation entries as individual JSON files. This separation enables efficient listing operations while preserving complete chat data with ETag support for client-side caching via ListETag and DetailETag generation.
Session Lifecycle: From Start to Completion
Starting a Chat Session
When an OpenAI-compatible request arrives, DS2API initializes storage through the startChatHistory function in internal/httpapi/openai/chat/chat_history.go. This function creates a new chatHistorySession wrapper that coordinates all subsequent storage operations:
func startChatHistory(store *chathistory.Store, r *http.Request,
a *auth.RequestAuth, stdReq promptcompat.StandardRequest) *chatHistorySession {
// Creates entry with UUID, timestamps, and caller metadata
entry, err := store.Start(chathistory.StartParams{
CallerID: strings.TrimSpace(a.CallerID),
AccountID: strings.TrimSpace(a.AccountID),
Model: strings.TrimSpace(stdReq.ResponseModel),
Stream: stdReq.Stream,
UserInput: extractSingleUserInput(stdReq.Messages),
Messages: extractAllMessages(stdReq.Messages),
HistoryText: stdReq.HistoryText,
FinalPrompt: stdReq.FinalPrompt,
})
// Returns session wrapper containing entry ID and store reference
}
The store.Start method atomically writes the initial entry to disk, capturing request metadata including authentication context, model selection, and message history before any streaming begins.
Incremental Updates During Streaming
During active streaming responses, DS2API throttles persistence to avoid excessive disk I/O. The chatHistorySession.progress method enforces a 250-millisecond minimum interval between updates:
func (s *chatHistorySession) progress(thinking, content string) {
if time.Since(s.lastPersist) < 250*time.Millisecond { return }
s.lastPersist = time.Now()
s.persistUpdate(chathistory.UpdateParams{
Status: "streaming",
ReasoningContent: thinking,
Content: content,
StatusCode: http.StatusOK,
ElapsedMs: time.Since(s.startedAt).Milliseconds(),
})
}
All updates route through store.Update(entryID, params), which modifies the detail file while maintaining the index's consistency.
Finalizing Responses
Upon completion, the session transitions to terminal states through three distinct pathways:
success(...)– Marks the entry as success, records final content chain-of-thought, captures token usage statistics, and marks the session completed.error(...)– Records error messages and sets status to error for debugging purposes.stopped(...)– Handles user-initiated interruptions, writing a stopped status while still capturing usage metrics viaopenaifmt.BuildChatUsage.
Persistent Storage Implementation
File Structure and Atomic Writes
The internal/chathistory/store.go file implements crash-safe persistence through writeFileAtomic, which writes to temporary files before atomic renaming via os.Rename. This guarantees that readers never encounter partially written JSON documents, even during system crashes.
The storage layout consists of:
- Index file (
chat_history.json): Contains aFilestruct with version tracking, configurable limit settings, global revision counters, and a slice ofSummaryEntryobjects for rapid listing. - Detail directory (
chat_history.json.d/): Houses individualchat_<uuid>.jsonfiles containing completeEntrystructs with full conversation history.
Entry Limits and Automatic Pruning
DS2API enforces configurable retention through DefaultLimit = 20 and MaxLimit = 50 constants. When rebuildIndexLocked detects limit violations, it automatically prunes older entries. Setting the limit to DisabledLimit (zero) entirely disables chat history storage, causing Store.Enabled() to return false and bypassing all persistence operations.
Admin API for History Management
The administrative HTTP interface in internal/httpapi/admin/history/handler_chat_history.go provides comprehensive CRUD operations:
| Method | Endpoint | Function |
|---|---|---|
GET |
/admin/history |
Lists all entries with ETag-supported caching (getChatHistory) |
GET |
/admin/history/{id} |
Retrieves specific entry details (getChatHistoryItem) |
DELETE |
/admin/history/{id} |
Removes individual chat records (deleteChatHistoryItem) |
DELETE |
/admin/history |
Clears entire history (clearChatHistory) |
PATCH |
/admin/history |
Adjusts retention limit via JSON payload (updateChatHistorySettings) |
All endpoints return standard HTTP status codes (400, 404, 500) with JSON responses, supporting conditional GET requests through ETag headers to minimize bandwidth.
Error Handling and Resilience
DS2API implements multi-layered error recovery to maintain operational continuity:
Missing Entry Recovery: When store.Update fails due to missing detail files (indicating potential disk corruption or external deletion), handlePersistError triggers retryMissingEntry. This mechanism attempts to reconstruct the entry with current data before abandoning persistence for that request.
Atomic Consistency: The writeFileAtomic function ensures that index and detail files remain synchronized, preventing orphan records or index corruption during write operations.
Graceful Degradation: If the configured storage path is inaccessible or initialization fails, the system automatically disables chat history functionality rather than crashing the API server.
Code Examples
Starting a Session Programmatically
Inject the store dependency and initialize tracking for incoming requests:
session := startChatHistory(
chatStore, // *chathistory.Store injected via DI
r, // *http.Request
authInfo, // *auth.RequestAuth
stdReq, // promptcompat.StandardRequest
)
Streaming Progress Updates
Update history during content generation loops:
// Inside your streaming implementation:
session.progress(thinkingAccumulator, partialContentBuffer)
Finalizing Successful Completion
session.success(
http.StatusOK,
finalThinkingContent, // accumulated reasoning
finalOutputContent, // model response
"stop", // finish reason
usageStatistics, // map[string]any with token counts
)
Admin Operations via cURL
List current chat history with ETag support:
curl -H "Accept: application/json" \
http://localhost:8080/admin/history
Delete a specific conversation:
curl -X DELETE http://localhost:8080/admin/history/chat_5f3b8c7e...
Adjust retention settings:
curl -X PATCH \
-H "Content-Type: application/json" \
-d '{"limit": 30}' \
http://localhost:8080/admin/history
Summary
- DS2API server-side chat history storage uses a file-based JSON architecture with atomic writes in
internal/chathistory/store.go. - Session management wraps storage operations through
chatHistorySessionininternal/httpapi/openai/chat/chat_history.go, throttling updates to 250ms intervals during streaming. - Dual-file structure separates lightweight index metadata from detailed conversation storage, supporting ETag caching and efficient administrative queries.
- Automatic pruning enforces configurable limits (
DefaultLimit = 20,MaxLimit = 50) while supporting complete disablement viaDisabledLimit. - Resilience features include atomic file writes, automatic entry recovery via
retryMissingEntry, and graceful disabling when storage is unavailable.
Frequently Asked Questions
How does DS2API ensure chat history data isn't corrupted during writes?
DS2API implements atomic file writes through the writeFileAtomic function in internal/chathistory/store.go. This method writes data to temporary files first, then uses os.Rename to move the completed file into place, ensuring that readers never encounter partially written JSON documents even if the process crashes mid-write.
Can I disable chat history storage entirely?
Yes. Set the storage limit to DisabledLimit (zero) or leave the chat history path unconfigured in your DS2API configuration. When Store.Enabled() returns false, the system bypasses all persistence operations in startChatHistory and runs in stateless mode without performance penalties.
What happens if a chat history file is deleted while a session is active?
The system detects missing entries through handlePersistError and attempts recovery via retryMissingEntry. This mechanism tries to recreate the entry file with current session data before giving up. If recovery fails, persistence disables for that specific request while allowing the API operation to continue, preventing cascading failures from affecting active chat sessions.
How does the admin API optimize bandwidth for large history lists?
The admin endpoints in internal/httpapi/admin/history/handler_chat_history.go generate ETag headers using ListETag and DetailETag functions from the store. Clients can send conditional GET requests with If-None-Match headers, and DS2API returns 304 Not Modified responses when data hasn't changed, eliminating unnecessary JSON serialization and network transfer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →