Skip to content

Commit 38f573c

Browse files
committed
release: merge develop into main for v0.20.2
2 parents c6d5a36 + 10d462b commit 38f573c

6 files changed

Lines changed: 109 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.20.2] - 2026-04-13
9+
10+
### Added
11+
12+
- **Durable chat history via JSONL logs** — chat messages are now append-only logged to `ADWs/logs/chat/{agent}_{session}.jsonl`. On session join, if the in-memory history is empty (e.g., after server restart), the JSONL log is read and restored automatically. This makes chat history survive restarts, `sessions.json` cleanups, and 7-day expiry
13+
814
## [0.20.1] - 2026-04-13
915

1016
### Added

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@evoapi/evo-nexus",
3-
"version": "0.20.1",
3+
"version": "0.20.2",
44
"description": "Unofficial open source toolkit for Claude Code — AI-powered business operating system",
55
"keywords": [
66
"claude-code",

dashboard/terminal-server/src/server.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { v4: uuidv4 } = require('uuid');
77
const ClaudeBridge = require('./claude-bridge');
88
const { ChatBridge } = require('./chat-bridge');
99
const SessionStore = require('./utils/session-store');
10+
const ChatLogger = require('./utils/chat-logger');
1011

1112
class TerminalServer {
1213
constructor(options = {}) {
@@ -20,6 +21,7 @@ class TerminalServer {
2021
this.claudeBridge = new ClaudeBridge();
2122
this.chatBridge = new ChatBridge();
2223
this.sessionStore = new SessionStore();
24+
this.chatLogger = new ChatLogger(this.baseFolder);
2325
this.autoSaveInterval = null;
2426
this.isShuttingDown = false;
2527

@@ -403,6 +405,7 @@ class TerminalServer {
403405
ts: Date.now(),
404406
};
405407
chatSession.chatHistory.push(userMsg);
408+
this.chatLogger.append(chatSession.agentName, wsInfo.claudeSessionId, userMsg);
406409

407410
// Accumulate assistant response for history
408411
let assistantBlocks = [];
@@ -475,12 +478,14 @@ class TerminalServer {
475478
}
476479
// Save assistant message to history
477480
if (assistantBlocks.length > 0) {
478-
chatSession.chatHistory.push({
481+
const assistantMsg = {
479482
role: 'assistant',
480483
blocks: assistantBlocks,
481484
ts: Date.now(),
482485
streaming: false,
483-
});
486+
};
487+
chatSession.chatHistory.push(assistantMsg);
488+
this.chatLogger.append(chatSession.agentName, wsInfo.claudeSessionId, assistantMsg);
484489
}
485490
this.saveSessionsToDisk();
486491
this.broadcastToSession(wsInfo.claudeSessionId, { type: 'chat_complete' });
@@ -548,14 +553,25 @@ class TerminalServer {
548553
session.lastActivity = new Date();
549554
session.lastAccessed = Date.now();
550555

556+
// Restore chat history from JSONL logs if session cache is empty
557+
let chatHistory = session.chatHistory || [];
558+
if (chatHistory.length === 0 && session.agentName && session.mode === 'chat') {
559+
const restored = this.chatLogger.read(session.agentName, claudeSessionId);
560+
if (restored.length > 0) {
561+
session.chatHistory = restored;
562+
chatHistory = restored;
563+
if (this.dev) console.log(`[chat-logger] Restored ${restored.length} messages for session ${claudeSessionId}`);
564+
}
565+
}
566+
551567
this.sendToWebSocket(wsInfo.ws, {
552568
type: 'session_joined',
553569
sessionId: claudeSessionId,
554570
sessionName: session.name,
555571
workingDir: session.workingDir,
556572
active: session.active,
557573
outputBuffer: session.outputBuffer.slice(-200),
558-
chatHistory: session.chatHistory || [],
574+
chatHistory,
559575
});
560576

561577
if (this.dev) console.log(`WebSocket ${wsId} joined session ${claudeSessionId}`);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* ChatLogger — append-only JSONL logs for chat conversations.
3+
*
4+
* Stores chat messages in workspace/ADWs/logs/chat/{agentName}_{sessionId}.jsonl
5+
* Each line is a JSON object: { role, text?, blocks?, files?, ts }
6+
*
7+
* This is the durable source of truth for chat history.
8+
* sessions.json is a fast-access cache; JSONL survives restarts and cleanups.
9+
*/
10+
11+
const fs = require('fs');
12+
const path = require('path');
13+
14+
class ChatLogger {
15+
constructor(workspaceRoot) {
16+
this.logsDir = path.join(workspaceRoot || process.cwd(), 'workspace', 'ADWs', 'logs', 'chat');
17+
this._ensureDir();
18+
}
19+
20+
_ensureDir() {
21+
try {
22+
fs.mkdirSync(this.logsDir, { recursive: true });
23+
} catch {}
24+
}
25+
26+
_logPath(agentName, sessionId) {
27+
const safe = (agentName || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
28+
const shortId = sessionId.slice(0, 8);
29+
return path.join(this.logsDir, `${safe}_${shortId}.jsonl`);
30+
}
31+
32+
/**
33+
* Append a message to the chat log.
34+
*/
35+
append(agentName, sessionId, message) {
36+
try {
37+
const logPath = this._logPath(agentName, sessionId);
38+
const line = JSON.stringify(message) + '\n';
39+
fs.appendFileSync(logPath, line, 'utf8');
40+
} catch (err) {
41+
console.error(`[chat-logger] Failed to append: ${err.message}`);
42+
}
43+
}
44+
45+
/**
46+
* Read full chat history from JSONL log.
47+
* Returns array of messages, or empty array if not found.
48+
*/
49+
read(agentName, sessionId) {
50+
try {
51+
const logPath = this._logPath(agentName, sessionId);
52+
if (!fs.existsSync(logPath)) return [];
53+
54+
const content = fs.readFileSync(logPath, 'utf8').trim();
55+
if (!content) return [];
56+
57+
const messages = [];
58+
for (const line of content.split('\n')) {
59+
if (!line.trim()) continue;
60+
try {
61+
messages.push(JSON.parse(line));
62+
} catch {
63+
// Skip malformed lines
64+
}
65+
}
66+
return messages;
67+
} catch (err) {
68+
console.error(`[chat-logger] Failed to read: ${err.message}`);
69+
return [];
70+
}
71+
}
72+
73+
/**
74+
* Check if a log exists for a session.
75+
*/
76+
exists(agentName, sessionId) {
77+
return fs.existsSync(this._logPath(agentName, sessionId));
78+
}
79+
}
80+
81+
module.exports = ChatLogger;

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "evo-nexus"
3-
version = "0.20.1"
3+
version = "0.20.2"
44
description = "Unofficial open source toolkit for Claude Code — AI-powered business operating system"
55
requires-python = ">=3.10"
66
dependencies = [

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)