@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.
@@ -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;