@ebowwa/glm-daemon 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/memory.ts +173 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebowwa/glm-daemon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Autonomous GLM 4.7 daemon with hooks, tools, teammates, and communication channels (Telegram, Discord)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"status": "bun run dist/bin/manager.js status"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@ebowwa/ai": "^0.1.
|
|
60
|
+
"@ebowwa/ai": "^0.1.2",
|
|
61
61
|
"@ebowwa/channel-types": "^0.1.1",
|
|
62
|
-
"@ebowwa/codespaces-types": "^1.
|
|
63
|
-
"@ebowwa/structured-prompts": "^0.2
|
|
64
|
-
"@ebowwa/teammates": "^0.1.
|
|
62
|
+
"@ebowwa/codespaces-types": "^1.4.0",
|
|
63
|
+
"@ebowwa/structured-prompts": "^0.3.2",
|
|
64
|
+
"@ebowwa/teammates": "^0.1.2",
|
|
65
65
|
"discord.js": "^14.16.3",
|
|
66
66
|
"node-telegram-bot-api": "^0.66.0",
|
|
67
67
|
"zod": "^4.3.5"
|
package/src/memory.ts
CHANGED
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tracks chat history per user/conversation with persistence.
|
|
5
5
|
* Can be used by Telegram, Discord, or any other channel.
|
|
6
|
+
*
|
|
7
|
+
* Storage format: JSONL (JSON Lines)
|
|
8
|
+
* Each line is a JSON object: {"id": "...", "messages": [...], "lastUpdated": timestamp}
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
} from "fs";
|
|
9
18
|
import { dirname } from "path";
|
|
10
19
|
|
|
11
20
|
/**
|
|
@@ -17,21 +26,35 @@ export interface ConversationMessage {
|
|
|
17
26
|
timestamp?: number;
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Internal storage format for JSONL
|
|
31
|
+
*/
|
|
32
|
+
interface ConversationEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
messages: ConversationMessage[];
|
|
35
|
+
lastUpdated: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
/**
|
|
21
39
|
* Configuration for ConversationMemory
|
|
22
40
|
*/
|
|
23
41
|
export interface ConversationMemoryConfig {
|
|
24
|
-
/** Path to persistence file (default: ./conversations.
|
|
42
|
+
/** Path to persistence file (default: ./conversations.jsonl) */
|
|
25
43
|
file?: string;
|
|
26
44
|
/** Maximum messages to keep per conversation (default: 10) */
|
|
27
45
|
maxMessages?: number;
|
|
28
46
|
/** Include timestamps in messages (default: true) */
|
|
29
47
|
includeTimestamps?: boolean;
|
|
48
|
+
/** Auto-migrate old .json files to .jsonl (default: true) */
|
|
49
|
+
autoMigrate?: boolean;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
/**
|
|
33
53
|
* Generic conversation memory that tracks chat history per conversation ID.
|
|
34
54
|
*
|
|
55
|
+
* Uses JSONL format for efficient append-only writes and streaming reads.
|
|
56
|
+
* Automatically migrates from old JSON format on first load.
|
|
57
|
+
*
|
|
35
58
|
* The ID type is generic (string | number) to support different channel types:
|
|
36
59
|
* - Telegram: number (chatId)
|
|
37
60
|
* - Discord: string (channelId/userId)
|
|
@@ -50,28 +73,85 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
50
73
|
private maxMessages: number;
|
|
51
74
|
private includeTimestamps: boolean;
|
|
52
75
|
private file: string;
|
|
76
|
+
private needsFullSave = false;
|
|
53
77
|
|
|
54
78
|
constructor(config: ConversationMemoryConfig = {}) {
|
|
55
|
-
|
|
79
|
+
// Default to .jsonl extension
|
|
80
|
+
const defaultFile = "./conversations.jsonl";
|
|
81
|
+
this.file = config.file || defaultFile;
|
|
82
|
+
|
|
83
|
+
// Auto-migrate from .json to .jsonl if needed
|
|
84
|
+
if (config.autoMigrate !== false && !existsSync(this.file)) {
|
|
85
|
+
this.migrateFromJson(this.file);
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
this.maxMessages = config.maxMessages ?? 10;
|
|
57
89
|
this.includeTimestamps = config.includeTimestamps ?? true;
|
|
58
90
|
this.load();
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
/**
|
|
62
|
-
*
|
|
94
|
+
* Migrate from old JSON format to JSONL
|
|
95
|
+
*/
|
|
96
|
+
private migrateFromJson(jsonlFile: string): void {
|
|
97
|
+
// Check for old .json file
|
|
98
|
+
const jsonFile = jsonlFile.replace(/\.jsonl$/, ".json");
|
|
99
|
+
if (!existsSync(jsonFile)) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(readFileSync(jsonFile, "utf-8"));
|
|
103
|
+
const entries: ConversationEntry[] = [];
|
|
104
|
+
|
|
105
|
+
for (const [id, messages] of Object.entries(data)) {
|
|
106
|
+
entries.push({
|
|
107
|
+
id,
|
|
108
|
+
messages: messages as ConversationMessage[],
|
|
109
|
+
lastUpdated: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write as JSONL
|
|
114
|
+
const dir = dirname(jsonlFile);
|
|
115
|
+
if (!existsSync(dir)) {
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const lines = entries.map((e) => JSON.stringify(e)).join("\n");
|
|
120
|
+
writeFileSync(jsonlFile, lines + (lines ? "\n" : ""));
|
|
121
|
+
|
|
122
|
+
// Backup old file instead of deleting
|
|
123
|
+
const backupFile = jsonFile + ".backup";
|
|
124
|
+
renameSync(jsonFile, backupFile);
|
|
125
|
+
|
|
126
|
+
console.log(`Migrated ${jsonFile} to ${jsonlFile} (${entries.length} conversations)`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Migration failed: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load conversations from JSONL file
|
|
134
|
+
* Uses synchronous read for constructor compatibility
|
|
63
135
|
*/
|
|
64
136
|
private load(): void {
|
|
65
137
|
if (!existsSync(this.file)) return;
|
|
138
|
+
|
|
66
139
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
140
|
+
const content = readFileSync(this.file, "utf-8");
|
|
141
|
+
const lines = content.split("\n");
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!line.trim()) continue;
|
|
145
|
+
try {
|
|
146
|
+
const entry: ConversationEntry = JSON.parse(line);
|
|
147
|
+
const key = this.parseKey(entry.id);
|
|
148
|
+
this.conversations.set(key, entry.messages);
|
|
149
|
+
} catch {
|
|
150
|
+
// Skip malformed lines
|
|
151
|
+
}
|
|
72
152
|
}
|
|
73
153
|
} catch {
|
|
74
|
-
//
|
|
154
|
+
// Start fresh on error
|
|
75
155
|
}
|
|
76
156
|
}
|
|
77
157
|
|
|
@@ -87,20 +167,69 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
87
167
|
}
|
|
88
168
|
|
|
89
169
|
/**
|
|
90
|
-
*
|
|
170
|
+
* Convert IdType to string for storage
|
|
91
171
|
*/
|
|
92
|
-
private
|
|
172
|
+
private stringifyKey(id: IdType): string {
|
|
173
|
+
return String(id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Save all conversations to disk (full rewrite)
|
|
178
|
+
* Used when entries are deleted or modified
|
|
179
|
+
*/
|
|
180
|
+
private saveFull(): void {
|
|
93
181
|
try {
|
|
94
|
-
// Ensure directory exists
|
|
95
182
|
const dir = dirname(this.file);
|
|
96
183
|
if (!existsSync(dir)) {
|
|
97
184
|
mkdirSync(dir, { recursive: true });
|
|
98
185
|
}
|
|
99
186
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
for (const [id, messages] of this.conversations) {
|
|
189
|
+
const entry: ConversationEntry = {
|
|
190
|
+
id: this.stringifyKey(id),
|
|
191
|
+
messages,
|
|
192
|
+
lastUpdated: Date.now(),
|
|
193
|
+
};
|
|
194
|
+
lines.push(JSON.stringify(entry));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
writeFileSync(this.file, lines.join("\n") + (lines.length ? "\n" : ""));
|
|
198
|
+
this.needsFullSave = false;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Save failed: ${error}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Append a single entry to the file (efficient for adds)
|
|
206
|
+
*/
|
|
207
|
+
private appendEntry(conversationId: IdType, messages: ConversationMessage[]): void {
|
|
208
|
+
try {
|
|
209
|
+
const dir = dirname(this.file);
|
|
210
|
+
if (!existsSync(dir)) {
|
|
211
|
+
mkdirSync(dir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const entry: ConversationEntry = {
|
|
215
|
+
id: this.stringifyKey(conversationId),
|
|
216
|
+
messages,
|
|
217
|
+
lastUpdated: Date.now(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Check if file exists and has content to determine if we need newline
|
|
221
|
+
const hasContent = existsSync(this.file) && readFileSync(this.file, "utf-8").length > 0;
|
|
222
|
+
const prefix = hasContent ? "" : "";
|
|
223
|
+
const line = JSON.stringify(entry) + "\n";
|
|
224
|
+
|
|
225
|
+
// We need to do a full save to replace the old entry for this conversation
|
|
226
|
+
// JSONL doesn't support in-place updates, so we mark for full save
|
|
227
|
+
this.needsFullSave = true;
|
|
228
|
+
|
|
229
|
+
// For now, do a full save since we need to replace the entry
|
|
230
|
+
this.saveFull();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error(`Append failed: ${error}`);
|
|
104
233
|
}
|
|
105
234
|
}
|
|
106
235
|
|
|
@@ -130,7 +259,8 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
130
259
|
history.splice(0, history.length - this.maxMessages);
|
|
131
260
|
}
|
|
132
261
|
|
|
133
|
-
|
|
262
|
+
// Save the updated conversation
|
|
263
|
+
this.appendEntry(conversationId, history);
|
|
134
264
|
}
|
|
135
265
|
|
|
136
266
|
/**
|
|
@@ -152,7 +282,7 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
152
282
|
*/
|
|
153
283
|
clear(conversationId: IdType): void {
|
|
154
284
|
this.conversations.delete(conversationId);
|
|
155
|
-
this.
|
|
285
|
+
this.saveFull();
|
|
156
286
|
}
|
|
157
287
|
|
|
158
288
|
/**
|
|
@@ -160,7 +290,7 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
160
290
|
*/
|
|
161
291
|
clearAll(): void {
|
|
162
292
|
this.conversations.clear();
|
|
163
|
-
this.
|
|
293
|
+
this.saveFull();
|
|
164
294
|
}
|
|
165
295
|
|
|
166
296
|
/**
|
|
@@ -195,6 +325,29 @@ export class ConversationMemory<IdType = string | number> {
|
|
|
195
325
|
content,
|
|
196
326
|
}));
|
|
197
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Force a full save (useful before shutdown)
|
|
331
|
+
*/
|
|
332
|
+
flush(): void {
|
|
333
|
+
if (this.needsFullSave || this.conversations.size > 0) {
|
|
334
|
+
this.saveFull();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the file path being used
|
|
340
|
+
*/
|
|
341
|
+
getFilePath(): string {
|
|
342
|
+
return this.file;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if the storage uses JSONL format
|
|
347
|
+
*/
|
|
348
|
+
isJsonl(): boolean {
|
|
349
|
+
return this.file.endsWith(".jsonl");
|
|
350
|
+
}
|
|
198
351
|
}
|
|
199
352
|
|
|
200
353
|
/**
|