@mariozechner/pi-mom 0.18.1 → 0.18.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/CHANGELOG.md +31 -11
- package/README.md +30 -18
- package/dist/agent.d.ts +14 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +294 -331
- package/dist/agent.js.map +1 -1
- package/dist/context.d.ts +132 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +538 -0
- package/dist/context.js.map +1 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +14 -1
- package/dist/log.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +166 -90
- package/dist/main.js.map +1 -1
- package/dist/slack.d.ts +86 -55
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +322 -418
- package/dist/slack.js.map +1 -1
- package/package.json +4 -3
package/dist/context.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management for mom.
|
|
3
|
+
*
|
|
4
|
+
* Mom uses two files per channel:
|
|
5
|
+
* - context.jsonl: Structured API messages for LLM context (same format as coding-agent sessions)
|
|
6
|
+
* - log.jsonl: Human-readable channel history for grep (no tool results)
|
|
7
|
+
*
|
|
8
|
+
* This module provides:
|
|
9
|
+
* - MomSessionManager: Adapts coding-agent's SessionManager for channel-based storage
|
|
10
|
+
* - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)
|
|
11
|
+
*/
|
|
12
|
+
import { loadSessionFromEntries, } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { randomBytes } from "crypto";
|
|
14
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
function uuidv4() {
|
|
17
|
+
const bytes = randomBytes(16);
|
|
18
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
19
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
20
|
+
const hex = bytes.toString("hex");
|
|
21
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
22
|
+
}
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// MomSessionManager - Channel-based session management
|
|
25
|
+
// ============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Session manager for mom, storing context per Slack channel.
|
|
28
|
+
*
|
|
29
|
+
* Unlike coding-agent which creates timestamped session files, mom uses
|
|
30
|
+
* a single context.jsonl per channel that persists across all @mentions.
|
|
31
|
+
*/
|
|
32
|
+
export class MomSessionManager {
|
|
33
|
+
sessionId;
|
|
34
|
+
contextFile;
|
|
35
|
+
logFile;
|
|
36
|
+
channelDir;
|
|
37
|
+
sessionInitialized = false;
|
|
38
|
+
inMemoryEntries = [];
|
|
39
|
+
pendingEntries = [];
|
|
40
|
+
constructor(channelDir, initialModel) {
|
|
41
|
+
this.channelDir = channelDir;
|
|
42
|
+
this.contextFile = join(channelDir, "context.jsonl");
|
|
43
|
+
this.logFile = join(channelDir, "log.jsonl");
|
|
44
|
+
// Ensure channel directory exists
|
|
45
|
+
if (!existsSync(channelDir)) {
|
|
46
|
+
mkdirSync(channelDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
// Load existing session or create new
|
|
49
|
+
if (existsSync(this.contextFile)) {
|
|
50
|
+
this.inMemoryEntries = this.loadEntriesFromFile();
|
|
51
|
+
this.sessionId = this.extractSessionId() || uuidv4();
|
|
52
|
+
this.sessionInitialized = this.inMemoryEntries.length > 0;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// New session - write header immediately
|
|
56
|
+
this.sessionId = uuidv4();
|
|
57
|
+
if (initialModel) {
|
|
58
|
+
this.writeSessionHeader(initialModel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Note: syncFromLog() is called explicitly from agent.ts with excludeTimestamp
|
|
62
|
+
}
|
|
63
|
+
/** Write session header to file (called on new session creation) */
|
|
64
|
+
writeSessionHeader(model) {
|
|
65
|
+
this.sessionInitialized = true;
|
|
66
|
+
const entry = {
|
|
67
|
+
type: "session",
|
|
68
|
+
id: this.sessionId,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
cwd: this.channelDir,
|
|
71
|
+
provider: model.provider,
|
|
72
|
+
modelId: model.id,
|
|
73
|
+
thinkingLevel: model.thinkingLevel || "off",
|
|
74
|
+
};
|
|
75
|
+
this.inMemoryEntries.push(entry);
|
|
76
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sync user messages from log.jsonl that aren't in context.jsonl.
|
|
80
|
+
*
|
|
81
|
+
* log.jsonl and context.jsonl must have the same user messages.
|
|
82
|
+
* This handles:
|
|
83
|
+
* - Backfilled messages (mom was offline)
|
|
84
|
+
* - Messages that arrived while mom was processing a previous turn
|
|
85
|
+
* - Channel chatter between @mentions
|
|
86
|
+
*
|
|
87
|
+
* Channel chatter is formatted as "[username]: message" to distinguish from direct @mentions.
|
|
88
|
+
*
|
|
89
|
+
* Called before each agent run.
|
|
90
|
+
*
|
|
91
|
+
* @param excludeSlackTs Slack timestamp of current message (will be added via prompt(), not sync)
|
|
92
|
+
*/
|
|
93
|
+
syncFromLog(excludeSlackTs) {
|
|
94
|
+
if (!existsSync(this.logFile))
|
|
95
|
+
return;
|
|
96
|
+
// Build set of Slack timestamps already in context
|
|
97
|
+
// We store slackTs in the message content or can extract from formatted messages
|
|
98
|
+
// For messages synced from log, we use the log's date as the entry timestamp
|
|
99
|
+
// For messages added via prompt(), they have different timestamps
|
|
100
|
+
// So we need to match by content OR by stored slackTs
|
|
101
|
+
const contextSlackTimestamps = new Set();
|
|
102
|
+
const contextMessageTexts = new Set();
|
|
103
|
+
for (const entry of this.inMemoryEntries) {
|
|
104
|
+
if (entry.type === "message") {
|
|
105
|
+
const msgEntry = entry;
|
|
106
|
+
// Store the entry timestamp (which is the log date for synced messages)
|
|
107
|
+
contextSlackTimestamps.add(entry.timestamp);
|
|
108
|
+
// Also store message text to catch duplicates added via prompt()
|
|
109
|
+
// AppMessage has different shapes, check for content property
|
|
110
|
+
const msg = msgEntry.message;
|
|
111
|
+
if (msg.role === "user" && msg.content !== undefined) {
|
|
112
|
+
const content = msg.content;
|
|
113
|
+
if (typeof content === "string") {
|
|
114
|
+
contextMessageTexts.add(content);
|
|
115
|
+
}
|
|
116
|
+
else if (Array.isArray(content)) {
|
|
117
|
+
for (const part of content) {
|
|
118
|
+
if (typeof part === "object" &&
|
|
119
|
+
part !== null &&
|
|
120
|
+
"type" in part &&
|
|
121
|
+
part.type === "text" &&
|
|
122
|
+
"text" in part) {
|
|
123
|
+
contextMessageTexts.add(part.text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Read log.jsonl and find user messages not in context
|
|
131
|
+
const logContent = readFileSync(this.logFile, "utf-8");
|
|
132
|
+
const logLines = logContent.trim().split("\n").filter(Boolean);
|
|
133
|
+
const newMessages = [];
|
|
134
|
+
for (const line of logLines) {
|
|
135
|
+
try {
|
|
136
|
+
const logMsg = JSON.parse(line);
|
|
137
|
+
const slackTs = logMsg.ts;
|
|
138
|
+
const date = logMsg.date;
|
|
139
|
+
if (!slackTs || !date)
|
|
140
|
+
continue;
|
|
141
|
+
// Skip the current message being processed (will be added via prompt())
|
|
142
|
+
if (excludeSlackTs && slackTs === excludeSlackTs)
|
|
143
|
+
continue;
|
|
144
|
+
// Skip bot messages - added through agent flow
|
|
145
|
+
if (logMsg.isBot)
|
|
146
|
+
continue;
|
|
147
|
+
// Skip if this date is already in context (was synced before)
|
|
148
|
+
if (contextSlackTimestamps.has(date))
|
|
149
|
+
continue;
|
|
150
|
+
// Build the message text as it would appear in context
|
|
151
|
+
const messageText = `[${logMsg.userName || logMsg.user || "unknown"}]: ${logMsg.text || ""}`;
|
|
152
|
+
// Skip if this exact message text is already in context (added via prompt())
|
|
153
|
+
if (contextMessageTexts.has(messageText))
|
|
154
|
+
continue;
|
|
155
|
+
const msgTime = new Date(date).getTime() || Date.now();
|
|
156
|
+
const userMessage = {
|
|
157
|
+
role: "user",
|
|
158
|
+
content: messageText,
|
|
159
|
+
timestamp: msgTime,
|
|
160
|
+
};
|
|
161
|
+
newMessages.push({ timestamp: date, slackTs, message: userMessage });
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Skip malformed lines
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (newMessages.length === 0)
|
|
168
|
+
return;
|
|
169
|
+
// Sort by timestamp and add to context
|
|
170
|
+
newMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
171
|
+
for (const { timestamp, message } of newMessages) {
|
|
172
|
+
const entry = {
|
|
173
|
+
type: "message",
|
|
174
|
+
timestamp, // Use log date as entry timestamp for consistent deduplication
|
|
175
|
+
message,
|
|
176
|
+
};
|
|
177
|
+
this.inMemoryEntries.push(entry);
|
|
178
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
extractSessionId() {
|
|
182
|
+
for (const entry of this.inMemoryEntries) {
|
|
183
|
+
if (entry.type === "session") {
|
|
184
|
+
return entry.id;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
loadEntriesFromFile() {
|
|
190
|
+
if (!existsSync(this.contextFile))
|
|
191
|
+
return [];
|
|
192
|
+
const content = readFileSync(this.contextFile, "utf8");
|
|
193
|
+
const entries = [];
|
|
194
|
+
const lines = content.trim().split("\n");
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (!line.trim())
|
|
197
|
+
continue;
|
|
198
|
+
try {
|
|
199
|
+
const entry = JSON.parse(line);
|
|
200
|
+
entries.push(entry);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Skip malformed lines
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return entries;
|
|
207
|
+
}
|
|
208
|
+
/** Initialize session with header if not already done */
|
|
209
|
+
startSession(state) {
|
|
210
|
+
if (this.sessionInitialized)
|
|
211
|
+
return;
|
|
212
|
+
this.sessionInitialized = true;
|
|
213
|
+
const entry = {
|
|
214
|
+
type: "session",
|
|
215
|
+
id: this.sessionId,
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
cwd: this.channelDir,
|
|
218
|
+
provider: state.model?.provider || "unknown",
|
|
219
|
+
modelId: state.model?.id || "unknown",
|
|
220
|
+
thinkingLevel: state.thinkingLevel,
|
|
221
|
+
};
|
|
222
|
+
this.inMemoryEntries.push(entry);
|
|
223
|
+
for (const pending of this.pendingEntries) {
|
|
224
|
+
this.inMemoryEntries.push(pending);
|
|
225
|
+
}
|
|
226
|
+
this.pendingEntries = [];
|
|
227
|
+
// Write to file
|
|
228
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
229
|
+
for (const memEntry of this.inMemoryEntries.slice(1)) {
|
|
230
|
+
appendFileSync(this.contextFile, JSON.stringify(memEntry) + "\n");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
saveMessage(message) {
|
|
234
|
+
const entry = {
|
|
235
|
+
type: "message",
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
message,
|
|
238
|
+
};
|
|
239
|
+
if (!this.sessionInitialized) {
|
|
240
|
+
this.pendingEntries.push(entry);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.inMemoryEntries.push(entry);
|
|
244
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
saveThinkingLevelChange(thinkingLevel) {
|
|
248
|
+
const entry = {
|
|
249
|
+
type: "thinking_level_change",
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
thinkingLevel,
|
|
252
|
+
};
|
|
253
|
+
if (!this.sessionInitialized) {
|
|
254
|
+
this.pendingEntries.push(entry);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.inMemoryEntries.push(entry);
|
|
258
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
saveModelChange(provider, modelId) {
|
|
262
|
+
const entry = {
|
|
263
|
+
type: "model_change",
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
provider,
|
|
266
|
+
modelId,
|
|
267
|
+
};
|
|
268
|
+
if (!this.sessionInitialized) {
|
|
269
|
+
this.pendingEntries.push(entry);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
this.inMemoryEntries.push(entry);
|
|
273
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
saveCompaction(entry) {
|
|
277
|
+
this.inMemoryEntries.push(entry);
|
|
278
|
+
appendFileSync(this.contextFile, JSON.stringify(entry) + "\n");
|
|
279
|
+
}
|
|
280
|
+
/** Load session with compaction support */
|
|
281
|
+
loadSession() {
|
|
282
|
+
const entries = this.loadEntries();
|
|
283
|
+
return loadSessionFromEntries(entries);
|
|
284
|
+
}
|
|
285
|
+
loadEntries() {
|
|
286
|
+
// Re-read from file to get latest state
|
|
287
|
+
if (existsSync(this.contextFile)) {
|
|
288
|
+
return this.loadEntriesFromFile();
|
|
289
|
+
}
|
|
290
|
+
return [...this.inMemoryEntries];
|
|
291
|
+
}
|
|
292
|
+
getSessionId() {
|
|
293
|
+
return this.sessionId;
|
|
294
|
+
}
|
|
295
|
+
getSessionFile() {
|
|
296
|
+
return this.contextFile;
|
|
297
|
+
}
|
|
298
|
+
/** Check if session should be initialized */
|
|
299
|
+
shouldInitializeSession(messages) {
|
|
300
|
+
if (this.sessionInitialized)
|
|
301
|
+
return false;
|
|
302
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
303
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
304
|
+
return userMessages.length >= 1 && assistantMessages.length >= 1;
|
|
305
|
+
}
|
|
306
|
+
/** Reset session (clears context.jsonl) */
|
|
307
|
+
reset() {
|
|
308
|
+
this.pendingEntries = [];
|
|
309
|
+
this.inMemoryEntries = [];
|
|
310
|
+
this.sessionInitialized = false;
|
|
311
|
+
this.sessionId = uuidv4();
|
|
312
|
+
// Truncate the context file
|
|
313
|
+
if (existsSync(this.contextFile)) {
|
|
314
|
+
writeFileSync(this.contextFile, "");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Compatibility methods for AgentSession
|
|
318
|
+
isEnabled() {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
setSessionFile(_path) {
|
|
322
|
+
// No-op for mom - we always use the channel's context.jsonl
|
|
323
|
+
}
|
|
324
|
+
loadModel() {
|
|
325
|
+
return this.loadSession().model;
|
|
326
|
+
}
|
|
327
|
+
loadThinkingLevel() {
|
|
328
|
+
return this.loadSession().thinkingLevel;
|
|
329
|
+
}
|
|
330
|
+
/** Not used by mom but required by AgentSession interface */
|
|
331
|
+
createBranchedSessionFromEntries(_entries, _branchBeforeIndex) {
|
|
332
|
+
return null; // Mom doesn't support branching
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const DEFAULT_COMPACTION = {
|
|
336
|
+
enabled: true,
|
|
337
|
+
reserveTokens: 16384,
|
|
338
|
+
keepRecentTokens: 20000,
|
|
339
|
+
};
|
|
340
|
+
const DEFAULT_RETRY = {
|
|
341
|
+
enabled: true,
|
|
342
|
+
maxRetries: 3,
|
|
343
|
+
baseDelayMs: 2000,
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Settings manager for mom.
|
|
347
|
+
* Stores settings in the workspace root directory.
|
|
348
|
+
*/
|
|
349
|
+
export class MomSettingsManager {
|
|
350
|
+
settingsPath;
|
|
351
|
+
settings;
|
|
352
|
+
constructor(workspaceDir) {
|
|
353
|
+
this.settingsPath = join(workspaceDir, "settings.json");
|
|
354
|
+
this.settings = this.load();
|
|
355
|
+
}
|
|
356
|
+
load() {
|
|
357
|
+
if (!existsSync(this.settingsPath)) {
|
|
358
|
+
return {};
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const content = readFileSync(this.settingsPath, "utf-8");
|
|
362
|
+
return JSON.parse(content);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
save() {
|
|
369
|
+
try {
|
|
370
|
+
const dir = dirname(this.settingsPath);
|
|
371
|
+
if (!existsSync(dir)) {
|
|
372
|
+
mkdirSync(dir, { recursive: true });
|
|
373
|
+
}
|
|
374
|
+
writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
console.error(`Warning: Could not save settings file: ${error}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
getCompactionSettings() {
|
|
381
|
+
return {
|
|
382
|
+
...DEFAULT_COMPACTION,
|
|
383
|
+
...this.settings.compaction,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
getCompactionEnabled() {
|
|
387
|
+
return this.settings.compaction?.enabled ?? DEFAULT_COMPACTION.enabled;
|
|
388
|
+
}
|
|
389
|
+
setCompactionEnabled(enabled) {
|
|
390
|
+
this.settings.compaction = { ...this.settings.compaction, enabled };
|
|
391
|
+
this.save();
|
|
392
|
+
}
|
|
393
|
+
getRetrySettings() {
|
|
394
|
+
return {
|
|
395
|
+
...DEFAULT_RETRY,
|
|
396
|
+
...this.settings.retry,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
getRetryEnabled() {
|
|
400
|
+
return this.settings.retry?.enabled ?? DEFAULT_RETRY.enabled;
|
|
401
|
+
}
|
|
402
|
+
setRetryEnabled(enabled) {
|
|
403
|
+
this.settings.retry = { ...this.settings.retry, enabled };
|
|
404
|
+
this.save();
|
|
405
|
+
}
|
|
406
|
+
getDefaultModel() {
|
|
407
|
+
return this.settings.defaultModel;
|
|
408
|
+
}
|
|
409
|
+
getDefaultProvider() {
|
|
410
|
+
return this.settings.defaultProvider;
|
|
411
|
+
}
|
|
412
|
+
setDefaultModelAndProvider(provider, modelId) {
|
|
413
|
+
this.settings.defaultProvider = provider;
|
|
414
|
+
this.settings.defaultModel = modelId;
|
|
415
|
+
this.save();
|
|
416
|
+
}
|
|
417
|
+
getDefaultThinkingLevel() {
|
|
418
|
+
return this.settings.defaultThinkingLevel || "off";
|
|
419
|
+
}
|
|
420
|
+
setDefaultThinkingLevel(level) {
|
|
421
|
+
this.settings.defaultThinkingLevel = level;
|
|
422
|
+
this.save();
|
|
423
|
+
}
|
|
424
|
+
// Compatibility methods for AgentSession
|
|
425
|
+
getQueueMode() {
|
|
426
|
+
return "one-at-a-time"; // Mom processes one message at a time
|
|
427
|
+
}
|
|
428
|
+
setQueueMode(_mode) {
|
|
429
|
+
// No-op for mom
|
|
430
|
+
}
|
|
431
|
+
getHookPaths() {
|
|
432
|
+
return []; // Mom doesn't use hooks
|
|
433
|
+
}
|
|
434
|
+
getHookTimeout() {
|
|
435
|
+
return 30000;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// Sync log.jsonl to context.jsonl
|
|
440
|
+
// ============================================================================
|
|
441
|
+
/**
|
|
442
|
+
* Sync user messages from log.jsonl to context.jsonl.
|
|
443
|
+
*
|
|
444
|
+
* This ensures that messages logged while mom wasn't running (channel chatter,
|
|
445
|
+
* backfilled messages, messages while busy) are added to the LLM context.
|
|
446
|
+
*
|
|
447
|
+
* @param channelDir - Path to channel directory
|
|
448
|
+
* @param excludeAfterTs - Don't sync messages with ts >= this value (they'll be handled by agent)
|
|
449
|
+
* @returns Number of messages synced
|
|
450
|
+
*/
|
|
451
|
+
export function syncLogToContext(channelDir, excludeAfterTs) {
|
|
452
|
+
const logFile = join(channelDir, "log.jsonl");
|
|
453
|
+
const contextFile = join(channelDir, "context.jsonl");
|
|
454
|
+
if (!existsSync(logFile))
|
|
455
|
+
return 0;
|
|
456
|
+
// Read all user messages from log.jsonl
|
|
457
|
+
const logContent = readFileSync(logFile, "utf-8");
|
|
458
|
+
const logLines = logContent.trim().split("\n").filter(Boolean);
|
|
459
|
+
const logMessages = [];
|
|
460
|
+
for (const line of logLines) {
|
|
461
|
+
try {
|
|
462
|
+
const entry = JSON.parse(line);
|
|
463
|
+
// Only sync user messages (not bot responses)
|
|
464
|
+
if (!entry.isBot && entry.ts && entry.text) {
|
|
465
|
+
// Skip if >= excludeAfterTs
|
|
466
|
+
if (excludeAfterTs && entry.ts >= excludeAfterTs)
|
|
467
|
+
continue;
|
|
468
|
+
logMessages.push(entry);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch { }
|
|
472
|
+
}
|
|
473
|
+
if (logMessages.length === 0)
|
|
474
|
+
return 0;
|
|
475
|
+
// Read existing timestamps from context.jsonl
|
|
476
|
+
const existingTs = new Set();
|
|
477
|
+
if (existsSync(contextFile)) {
|
|
478
|
+
const contextContent = readFileSync(contextFile, "utf-8");
|
|
479
|
+
const contextLines = contextContent.trim().split("\n").filter(Boolean);
|
|
480
|
+
for (const line of contextLines) {
|
|
481
|
+
try {
|
|
482
|
+
const entry = JSON.parse(line);
|
|
483
|
+
if (entry.type === "message" && entry.message?.role === "user" && entry.message?.timestamp) {
|
|
484
|
+
// Extract ts from timestamp (ms -> slack ts format for comparison)
|
|
485
|
+
// We store the original slack ts in a way we can recover
|
|
486
|
+
// Actually, let's just check by content match since ts formats differ
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch { }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// For deduplication, we need to track what's already in context
|
|
493
|
+
// Read context and extract user message content
|
|
494
|
+
const existingMessages = new Set();
|
|
495
|
+
if (existsSync(contextFile)) {
|
|
496
|
+
const contextContent = readFileSync(contextFile, "utf-8");
|
|
497
|
+
const contextLines = contextContent.trim().split("\n").filter(Boolean);
|
|
498
|
+
for (const line of contextLines) {
|
|
499
|
+
try {
|
|
500
|
+
const entry = JSON.parse(line);
|
|
501
|
+
if (entry.type === "message" && entry.message?.role === "user") {
|
|
502
|
+
const content = typeof entry.message.content === "string" ? entry.message.content : entry.message.content?.[0]?.text;
|
|
503
|
+
if (content)
|
|
504
|
+
existingMessages.add(content);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch { }
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Add missing messages to context.jsonl
|
|
511
|
+
let syncedCount = 0;
|
|
512
|
+
for (const msg of logMessages) {
|
|
513
|
+
const userName = msg.userName || msg.user;
|
|
514
|
+
const content = `[${userName}]: ${msg.text}`;
|
|
515
|
+
// Skip if already in context
|
|
516
|
+
if (existingMessages.has(content))
|
|
517
|
+
continue;
|
|
518
|
+
const timestamp = Math.floor(parseFloat(msg.ts) * 1000);
|
|
519
|
+
const entry = {
|
|
520
|
+
type: "message",
|
|
521
|
+
timestamp: new Date(timestamp).toISOString(),
|
|
522
|
+
message: {
|
|
523
|
+
role: "user",
|
|
524
|
+
content,
|
|
525
|
+
timestamp,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
// Ensure directory exists
|
|
529
|
+
if (!existsSync(channelDir)) {
|
|
530
|
+
mkdirSync(channelDir, { recursive: true });
|
|
531
|
+
}
|
|
532
|
+
appendFileSync(contextFile, JSON.stringify(entry) + "\n");
|
|
533
|
+
existingMessages.add(content); // Track to avoid duplicates within this sync
|
|
534
|
+
syncedCount++;
|
|
535
|
+
}
|
|
536
|
+
return syncedCount;
|
|
537
|
+
}
|
|
538
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context management for mom.\n *\n * Mom uses two files per channel:\n * - context.jsonl: Structured API messages for LLM context (same format as coding-agent sessions)\n * - log.jsonl: Human-readable channel history for grep (no tool results)\n *\n * This module provides:\n * - MomSessionManager: Adapts coding-agent's SessionManager for channel-based storage\n * - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)\n */\n\nimport type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\ttype CompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\ttype SessionEntry,\n\ttype SessionHeader,\n\ttype SessionMessageEntry,\n\ttype ThinkingLevelChangeEntry,\n} from \"@mariozechner/pi-coding-agent\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// MomSessionManager - Channel-based session management\n// ============================================================================\n\n/**\n * Session manager for mom, storing context per Slack channel.\n *\n * Unlike coding-agent which creates timestamped session files, mom uses\n * a single context.jsonl per channel that persists across all @mentions.\n */\nexport class MomSessionManager {\n\tprivate sessionId: string;\n\tprivate contextFile: string;\n\tprivate logFile: string;\n\tprivate channelDir: string;\n\tprivate sessionInitialized: boolean = false;\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\tprivate pendingEntries: SessionEntry[] = [];\n\n\tconstructor(channelDir: string, initialModel?: { provider: string; id: string; thinkingLevel?: string }) {\n\t\tthis.channelDir = channelDir;\n\t\tthis.contextFile = join(channelDir, \"context.jsonl\");\n\t\tthis.logFile = join(channelDir, \"log.jsonl\");\n\n\t\t// Ensure channel directory exists\n\t\tif (!existsSync(channelDir)) {\n\t\t\tmkdirSync(channelDir, { recursive: true });\n\t\t}\n\n\t\t// Load existing session or create new\n\t\tif (existsSync(this.contextFile)) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\tthis.sessionId = this.extractSessionId() || uuidv4();\n\t\t\tthis.sessionInitialized = this.inMemoryEntries.length > 0;\n\t\t} else {\n\t\t\t// New session - write header immediately\n\t\t\tthis.sessionId = uuidv4();\n\t\t\tif (initialModel) {\n\t\t\t\tthis.writeSessionHeader(initialModel);\n\t\t\t}\n\t\t}\n\t\t// Note: syncFromLog() is called explicitly from agent.ts with excludeTimestamp\n\t}\n\n\t/** Write session header to file (called on new session creation) */\n\tprivate writeSessionHeader(model: { provider: string; id: string; thinkingLevel?: string }): void {\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: this.channelDir,\n\t\t\tprovider: model.provider,\n\t\t\tmodelId: model.id,\n\t\t\tthinkingLevel: model.thinkingLevel || \"off\",\n\t\t};\n\n\t\tthis.inMemoryEntries.push(entry);\n\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t}\n\n\t/**\n\t * Sync user messages from log.jsonl that aren't in context.jsonl.\n\t *\n\t * log.jsonl and context.jsonl must have the same user messages.\n\t * This handles:\n\t * - Backfilled messages (mom was offline)\n\t * - Messages that arrived while mom was processing a previous turn\n\t * - Channel chatter between @mentions\n\t *\n\t * Channel chatter is formatted as \"[username]: message\" to distinguish from direct @mentions.\n\t *\n\t * Called before each agent run.\n\t *\n\t * @param excludeSlackTs Slack timestamp of current message (will be added via prompt(), not sync)\n\t */\n\tsyncFromLog(excludeSlackTs?: string): void {\n\t\tif (!existsSync(this.logFile)) return;\n\n\t\t// Build set of Slack timestamps already in context\n\t\t// We store slackTs in the message content or can extract from formatted messages\n\t\t// For messages synced from log, we use the log's date as the entry timestamp\n\t\t// For messages added via prompt(), they have different timestamps\n\t\t// So we need to match by content OR by stored slackTs\n\t\tconst contextSlackTimestamps = new Set<string>();\n\t\tconst contextMessageTexts = new Set<string>();\n\n\t\tfor (const entry of this.inMemoryEntries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tconst msgEntry = entry as SessionMessageEntry;\n\t\t\t\t// Store the entry timestamp (which is the log date for synced messages)\n\t\t\t\tcontextSlackTimestamps.add(entry.timestamp);\n\n\t\t\t\t// Also store message text to catch duplicates added via prompt()\n\t\t\t\t// AppMessage has different shapes, check for content property\n\t\t\t\tconst msg = msgEntry.message as { role: string; content?: unknown };\n\t\t\t\tif (msg.role === \"user\" && msg.content !== undefined) {\n\t\t\t\t\tconst content = msg.content;\n\t\t\t\t\tif (typeof content === \"string\") {\n\t\t\t\t\t\tcontextMessageTexts.add(content);\n\t\t\t\t\t} else if (Array.isArray(content)) {\n\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\ttypeof part === \"object\" &&\n\t\t\t\t\t\t\t\tpart !== null &&\n\t\t\t\t\t\t\t\t\"type\" in part &&\n\t\t\t\t\t\t\t\tpart.type === \"text\" &&\n\t\t\t\t\t\t\t\t\"text\" in part\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontextMessageTexts.add((part as { type: \"text\"; text: string }).text);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Read log.jsonl and find user messages not in context\n\t\tconst logContent = readFileSync(this.logFile, \"utf-8\");\n\t\tconst logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n\t\tinterface LogMessage {\n\t\t\tdate?: string;\n\t\t\tts?: string;\n\t\t\tuser?: string;\n\t\t\tuserName?: string;\n\t\t\ttext?: string;\n\t\t\tisBot?: boolean;\n\t\t}\n\n\t\tconst newMessages: Array<{ timestamp: string; slackTs: string; message: AppMessage }> = [];\n\n\t\tfor (const line of logLines) {\n\t\t\ttry {\n\t\t\t\tconst logMsg: LogMessage = JSON.parse(line);\n\n\t\t\t\tconst slackTs = logMsg.ts;\n\t\t\t\tconst date = logMsg.date;\n\t\t\t\tif (!slackTs || !date) continue;\n\n\t\t\t\t// Skip the current message being processed (will be added via prompt())\n\t\t\t\tif (excludeSlackTs && slackTs === excludeSlackTs) continue;\n\n\t\t\t\t// Skip bot messages - added through agent flow\n\t\t\t\tif (logMsg.isBot) continue;\n\n\t\t\t\t// Skip if this date is already in context (was synced before)\n\t\t\t\tif (contextSlackTimestamps.has(date)) continue;\n\n\t\t\t\t// Build the message text as it would appear in context\n\t\t\t\tconst messageText = `[${logMsg.userName || logMsg.user || \"unknown\"}]: ${logMsg.text || \"\"}`;\n\n\t\t\t\t// Skip if this exact message text is already in context (added via prompt())\n\t\t\t\tif (contextMessageTexts.has(messageText)) continue;\n\n\t\t\t\tconst msgTime = new Date(date).getTime() || Date.now();\n\t\t\t\tconst userMessage: AppMessage = {\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: messageText,\n\t\t\t\t\ttimestamp: msgTime,\n\t\t\t\t};\n\n\t\t\t\tnewMessages.push({ timestamp: date, slackTs, message: userMessage });\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\tif (newMessages.length === 0) return;\n\n\t\t// Sort by timestamp and add to context\n\t\tnewMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n\n\t\tfor (const { timestamp, message } of newMessages) {\n\t\t\tconst entry: SessionMessageEntry = {\n\t\t\t\ttype: \"message\",\n\t\t\t\ttimestamp, // Use log date as entry timestamp for consistent deduplication\n\t\t\t\tmessage,\n\t\t\t};\n\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tprivate extractSessionId(): string | null {\n\t\tfor (const entry of this.inMemoryEntries) {\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\treturn entry.id;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.contextFile)) return [];\n\n\t\tconst content = readFileSync(this.contextFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/** Initialize session with header if not already done */\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: this.channelDir,\n\t\t\tprovider: state.model?.provider || \"unknown\",\n\t\t\tmodelId: state.model?.id || \"unknown\",\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file\n\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\tappendFileSync(this.contextFile, JSON.stringify(memEntry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveMessage(message: AppMessage): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\tthis.inMemoryEntries.push(entry);\n\t\tappendFileSync(this.contextFile, JSON.stringify(entry) + \"\\n\");\n\t}\n\n\t/** Load session with compaction support */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\tloadEntries(): SessionEntry[] {\n\t\t// Re-read from file to get latest state\n\t\tif (existsSync(this.contextFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.contextFile;\n\t}\n\n\t/** Check if session should be initialized */\n\tshouldInitializeSession(messages: AppMessage[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/** Reset session (clears context.jsonl) */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.sessionId = uuidv4();\n\t\t// Truncate the context file\n\t\tif (existsSync(this.contextFile)) {\n\t\t\twriteFileSync(this.contextFile, \"\");\n\t\t}\n\t}\n\n\t// Compatibility methods for AgentSession\n\tisEnabled(): boolean {\n\t\treturn true;\n\t}\n\n\tsetSessionFile(_path: string): void {\n\t\t// No-op for mom - we always use the channel's context.jsonl\n\t}\n\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/** Not used by mom but required by AgentSession interface */\n\tcreateBranchedSessionFromEntries(_entries: SessionEntry[], _branchBeforeIndex: number): string | null {\n\t\treturn null; // Mom doesn't support branching\n\t}\n}\n\n// ============================================================================\n// MomSettingsManager - Simple settings for mom\n// ============================================================================\n\nexport interface MomCompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport interface MomRetrySettings {\n\tenabled: boolean;\n\tmaxRetries: number;\n\tbaseDelayMs: number;\n}\n\nexport interface MomSettings {\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\";\n\tcompaction?: Partial<MomCompactionSettings>;\n\tretry?: Partial<MomRetrySettings>;\n}\n\nconst DEFAULT_COMPACTION: MomCompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\nconst DEFAULT_RETRY: MomRetrySettings = {\n\tenabled: true,\n\tmaxRetries: 3,\n\tbaseDelayMs: 2000,\n};\n\n/**\n * Settings manager for mom.\n * Stores settings in the workspace root directory.\n */\nexport class MomSettingsManager {\n\tprivate settingsPath: string;\n\tprivate settings: MomSettings;\n\n\tconstructor(workspaceDir: string) {\n\t\tthis.settingsPath = join(workspaceDir, \"settings.json\");\n\t\tthis.settings = this.load();\n\t}\n\n\tprivate load(): MomSettings {\n\t\tif (!existsSync(this.settingsPath)) {\n\t\t\treturn {};\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(this.settingsPath, \"utf-8\");\n\t\t\treturn JSON.parse(content);\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate save(): void {\n\t\ttry {\n\t\t\tconst dir = dirname(this.settingsPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t}\n\t\t\twriteFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(`Warning: Could not save settings file: ${error}`);\n\t\t}\n\t}\n\n\tgetCompactionSettings(): MomCompactionSettings {\n\t\treturn {\n\t\t\t...DEFAULT_COMPACTION,\n\t\t\t...this.settings.compaction,\n\t\t};\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? DEFAULT_COMPACTION.enabled;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tthis.settings.compaction = { ...this.settings.compaction, enabled };\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): MomRetrySettings {\n\t\treturn {\n\t\t\t...DEFAULT_RETRY,\n\t\t\t...this.settings.retry,\n\t\t};\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? DEFAULT_RETRY.enabled;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tthis.settings.retry = { ...this.settings.retry, enabled };\n\t\tthis.save();\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.settings.defaultProvider = provider;\n\t\tthis.settings.defaultModel = modelId;\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): string {\n\t\treturn this.settings.defaultThinkingLevel || \"off\";\n\t}\n\n\tsetDefaultThinkingLevel(level: string): void {\n\t\tthis.settings.defaultThinkingLevel = level as MomSettings[\"defaultThinkingLevel\"];\n\t\tthis.save();\n\t}\n\n\t// Compatibility methods for AgentSession\n\tgetQueueMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn \"one-at-a-time\"; // Mom processes one message at a time\n\t}\n\n\tsetQueueMode(_mode: \"all\" | \"one-at-a-time\"): void {\n\t\t// No-op for mom\n\t}\n\n\tgetHookPaths(): string[] {\n\t\treturn []; // Mom doesn't use hooks\n\t}\n\n\tgetHookTimeout(): number {\n\t\treturn 30000;\n\t}\n}\n\n// ============================================================================\n// Sync log.jsonl to context.jsonl\n// ============================================================================\n\n/**\n * Sync user messages from log.jsonl to context.jsonl.\n *\n * This ensures that messages logged while mom wasn't running (channel chatter,\n * backfilled messages, messages while busy) are added to the LLM context.\n *\n * @param channelDir - Path to channel directory\n * @param excludeAfterTs - Don't sync messages with ts >= this value (they'll be handled by agent)\n * @returns Number of messages synced\n */\nexport function syncLogToContext(channelDir: string, excludeAfterTs?: string): number {\n\tconst logFile = join(channelDir, \"log.jsonl\");\n\tconst contextFile = join(channelDir, \"context.jsonl\");\n\n\tif (!existsSync(logFile)) return 0;\n\n\t// Read all user messages from log.jsonl\n\tconst logContent = readFileSync(logFile, \"utf-8\");\n\tconst logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n\tinterface LogEntry {\n\t\tts: string;\n\t\tuser: string;\n\t\tuserName?: string;\n\t\ttext: string;\n\t\tisBot: boolean;\n\t}\n\n\tconst logMessages: LogEntry[] = [];\n\tfor (const line of logLines) {\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as LogEntry;\n\t\t\t// Only sync user messages (not bot responses)\n\t\t\tif (!entry.isBot && entry.ts && entry.text) {\n\t\t\t\t// Skip if >= excludeAfterTs\n\t\t\t\tif (excludeAfterTs && entry.ts >= excludeAfterTs) continue;\n\t\t\t\tlogMessages.push(entry);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\tif (logMessages.length === 0) return 0;\n\n\t// Read existing timestamps from context.jsonl\n\tconst existingTs = new Set<string>();\n\tif (existsSync(contextFile)) {\n\t\tconst contextContent = readFileSync(contextFile, \"utf-8\");\n\t\tconst contextLines = contextContent.trim().split(\"\\n\").filter(Boolean);\n\t\tfor (const line of contextLines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"user\" && entry.message?.timestamp) {\n\t\t\t\t\t// Extract ts from timestamp (ms -> slack ts format for comparison)\n\t\t\t\t\t// We store the original slack ts in a way we can recover\n\t\t\t\t\t// Actually, let's just check by content match since ts formats differ\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t}\n\n\t// For deduplication, we need to track what's already in context\n\t// Read context and extract user message content\n\tconst existingMessages = new Set<string>();\n\tif (existsSync(contextFile)) {\n\t\tconst contextContent = readFileSync(contextFile, \"utf-8\");\n\t\tconst contextLines = contextContent.trim().split(\"\\n\").filter(Boolean);\n\t\tfor (const line of contextLines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"user\") {\n\t\t\t\t\tconst content =\n\t\t\t\t\t\ttypeof entry.message.content === \"string\" ? entry.message.content : entry.message.content?.[0]?.text;\n\t\t\t\t\tif (content) existingMessages.add(content);\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t}\n\n\t// Add missing messages to context.jsonl\n\tlet syncedCount = 0;\n\tfor (const msg of logMessages) {\n\t\tconst userName = msg.userName || msg.user;\n\t\tconst content = `[${userName}]: ${msg.text}`;\n\n\t\t// Skip if already in context\n\t\tif (existingMessages.has(content)) continue;\n\n\t\tconst timestamp = Math.floor(parseFloat(msg.ts) * 1000);\n\t\tconst entry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date(timestamp).toISOString(),\n\t\t\tmessage: {\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent,\n\t\t\t\ttimestamp,\n\t\t\t},\n\t\t};\n\n\t\t// Ensure directory exists\n\t\tif (!existsSync(channelDir)) {\n\t\t\tmkdirSync(channelDir, { recursive: true });\n\t\t}\n\n\t\tappendFileSync(contextFile, JSON.stringify(entry) + \"\\n\");\n\t\texistingMessages.add(content); // Track to avoid duplicates within this sync\n\t\tsyncedCount++;\n\t}\n\n\treturn syncedCount;\n}\n"]}
|
package/dist/log.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare function logUsageSummary(ctx: LogContext, usage: {
|
|
|
29
29
|
cacheWrite: number;
|
|
30
30
|
total: number;
|
|
31
31
|
};
|
|
32
|
-
}): string;
|
|
32
|
+
}, contextTokens?: number, contextWindow?: number): string;
|
|
33
33
|
export declare function logStartup(workingDir: string, sandbox: string): void;
|
|
34
34
|
export declare function logConnected(): void;
|
|
35
35
|
export declare function logDisconnected(): void;
|