@mariozechner/pi-mom 0.42.3 → 0.42.5

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/dist/context.js CHANGED
@@ -6,307 +6,108 @@
6
6
  * - log.jsonl: Human-readable channel history for grep (no tool results)
7
7
  *
8
8
  * This module provides:
9
- * - MomSessionManager: Adapts coding-agent's SessionManager for channel-based storage
9
+ * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager
10
10
  * - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)
11
11
  */
12
- import { buildSessionContext, } from "@mariozechner/pi-coding-agent";
13
- import { randomBytes } from "crypto";
14
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
13
  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
14
  /**
27
- * Session manager for mom, storing context per Slack channel.
15
+ * Sync user messages from log.jsonl to SessionManager.
16
+ *
17
+ * This ensures that messages logged while mom wasn't running (channel chatter,
18
+ * backfilled messages, messages while busy) are added to the LLM context.
28
19
  *
29
- * Unlike coding-agent which creates timestamped session files, mom uses
30
- * a single context.jsonl per channel that persists across all @mentions.
20
+ * @param sessionManager - The SessionManager to sync to
21
+ * @param channelDir - Path to channel directory containing log.jsonl
22
+ * @param excludeSlackTs - Slack timestamp of current message (will be added via prompt(), not sync)
23
+ * @returns Number of messages synced
31
24
  */
32
- export class MomSessionManager {
33
- sessionId;
34
- contextFile;
35
- logFile;
36
- channelDir;
37
- flushed = false;
38
- inMemoryEntries = [];
39
- leafId = null;
40
- constructor(channelDir) {
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._updateLeafId();
53
- this.flushed = true;
54
- }
55
- else {
56
- this.sessionId = uuidv4();
57
- this.inMemoryEntries = [
58
- {
59
- type: "session",
60
- version: 2,
61
- id: this.sessionId,
62
- timestamp: new Date().toISOString(),
63
- cwd: this.channelDir,
64
- },
65
- ];
66
- }
67
- // Note: syncFromLog() is called explicitly from agent.ts with excludeTimestamp
68
- }
69
- _updateLeafId() {
70
- for (let i = this.inMemoryEntries.length - 1; i >= 0; i--) {
71
- const entry = this.inMemoryEntries[i];
72
- if (entry.type !== "session") {
73
- this.leafId = entry.id;
74
- return;
75
- }
76
- }
77
- this.leafId = null;
78
- }
79
- _createEntryBase() {
80
- const id = uuidv4();
81
- const base = {
82
- id,
83
- parentId: this.leafId,
84
- timestamp: new Date().toISOString(),
85
- };
86
- this.leafId = id;
87
- return base;
88
- }
89
- _persist(entry) {
90
- const hasAssistant = this.inMemoryEntries.some((e) => e.type === "message" && e.message.role === "assistant");
91
- if (!hasAssistant)
92
- return;
93
- if (!this.flushed) {
94
- for (const e of this.inMemoryEntries) {
95
- appendFileSync(this.contextFile, `${JSON.stringify(e)}\n`);
96
- }
97
- this.flushed = true;
98
- }
99
- else {
100
- appendFileSync(this.contextFile, `${JSON.stringify(entry)}\n`);
101
- }
102
- }
103
- /**
104
- * Sync user messages from log.jsonl that aren't in context.jsonl.
105
- *
106
- * log.jsonl and context.jsonl must have the same user messages.
107
- * This handles:
108
- * - Backfilled messages (mom was offline)
109
- * - Messages that arrived while mom was processing a previous turn
110
- * - Channel chatter between @mentions
111
- *
112
- * Channel chatter is formatted as "[username]: message" to distinguish from direct @mentions.
113
- *
114
- * Called before each agent run.
115
- *
116
- * @param excludeSlackTs Slack timestamp of current message (will be added via prompt(), not sync)
117
- */
118
- syncFromLog(excludeSlackTs) {
119
- if (!existsSync(this.logFile))
120
- return;
121
- // Build set of Slack timestamps already in context
122
- // We store slackTs in the message content or can extract from formatted messages
123
- // For messages synced from log, we use the log's date as the entry timestamp
124
- // For messages added via prompt(), they have different timestamps
125
- // So we need to match by content OR by stored slackTs
126
- const contextSlackTimestamps = new Set();
127
- const contextMessageTexts = new Set();
128
- for (const entry of this.inMemoryEntries) {
129
- if (entry.type === "message") {
130
- const msgEntry = entry;
131
- // Store the entry timestamp (which is the log date for synced messages)
132
- contextSlackTimestamps.add(entry.timestamp);
133
- // Also store message text to catch duplicates added via prompt()
134
- // AgentMessage has different shapes, check for content property
135
- const msg = msgEntry.message;
136
- if (msg.role === "user" && msg.content !== undefined) {
137
- const content = msg.content;
138
- if (typeof content === "string") {
139
- contextMessageTexts.add(content);
25
+ export function syncLogToSessionManager(sessionManager, channelDir, excludeSlackTs) {
26
+ const logFile = join(channelDir, "log.jsonl");
27
+ if (!existsSync(logFile))
28
+ return 0;
29
+ // Build set of existing message content from session
30
+ const existingMessages = new Set();
31
+ for (const entry of sessionManager.getEntries()) {
32
+ if (entry.type === "message") {
33
+ const msgEntry = entry;
34
+ const msg = msgEntry.message;
35
+ if (msg.role === "user" && msg.content !== undefined) {
36
+ const content = msg.content;
37
+ if (typeof content === "string") {
38
+ // Strip timestamp prefix for comparison (live messages have it, synced don't)
39
+ // Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text
40
+ let normalized = content.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\] /, "");
41
+ // Strip attachments section
42
+ const attachmentsIdx = normalized.indexOf("\n\n<slack_attachments>\n");
43
+ if (attachmentsIdx !== -1) {
44
+ normalized = normalized.substring(0, attachmentsIdx);
140
45
  }
141
- else if (Array.isArray(content)) {
142
- for (const part of content) {
143
- if (typeof part === "object" &&
144
- part !== null &&
145
- "type" in part &&
146
- part.type === "text" &&
147
- "text" in part) {
148
- contextMessageTexts.add(part.text);
46
+ existingMessages.add(normalized);
47
+ }
48
+ else if (Array.isArray(content)) {
49
+ for (const part of content) {
50
+ if (typeof part === "object" &&
51
+ part !== null &&
52
+ "type" in part &&
53
+ part.type === "text" &&
54
+ "text" in part) {
55
+ let normalized = part.text;
56
+ normalized = normalized.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\] /, "");
57
+ const attachmentsIdx = normalized.indexOf("\n\n<slack_attachments>\n");
58
+ if (attachmentsIdx !== -1) {
59
+ normalized = normalized.substring(0, attachmentsIdx);
149
60
  }
61
+ existingMessages.add(normalized);
150
62
  }
151
63
  }
152
64
  }
153
65
  }
154
66
  }
155
- // Read log.jsonl and find user messages not in context
156
- const logContent = readFileSync(this.logFile, "utf-8");
157
- const logLines = logContent.trim().split("\n").filter(Boolean);
158
- const newMessages = [];
159
- for (const line of logLines) {
160
- try {
161
- const logMsg = JSON.parse(line);
162
- const slackTs = logMsg.ts;
163
- const date = logMsg.date;
164
- if (!slackTs || !date)
165
- continue;
166
- // Skip the current message being processed (will be added via prompt())
167
- if (excludeSlackTs && slackTs === excludeSlackTs)
168
- continue;
169
- // Skip bot messages - added through agent flow
170
- if (logMsg.isBot)
171
- continue;
172
- // Skip if this date is already in context (was synced before)
173
- if (contextSlackTimestamps.has(date))
174
- continue;
175
- // Build the message text as it would appear in context
176
- const messageText = `[${logMsg.userName || logMsg.user || "unknown"}]: ${logMsg.text || ""}`;
177
- // Skip if this exact message text is already in context (added via prompt())
178
- if (contextMessageTexts.has(messageText))
179
- continue;
180
- const msgTime = new Date(date).getTime() || Date.now();
181
- const userMessage = {
182
- role: "user",
183
- content: messageText,
184
- timestamp: msgTime,
185
- };
186
- newMessages.push({ timestamp: date, slackTs, message: userMessage });
187
- }
188
- catch {
189
- // Skip malformed lines
190
- }
191
- }
192
- if (newMessages.length === 0)
193
- return;
194
- // Sort by timestamp and add to context
195
- newMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
196
- for (const { timestamp, message } of newMessages) {
197
- const id = uuidv4();
198
- const entry = {
199
- type: "message",
200
- id,
201
- parentId: this.leafId,
202
- timestamp, // Use log date as entry timestamp for consistent deduplication
203
- message,
204
- };
205
- this.leafId = id;
206
- this.inMemoryEntries.push(entry);
207
- appendFileSync(this.contextFile, `${JSON.stringify(entry)}\n`);
208
- }
209
- }
210
- extractSessionId() {
211
- for (const entry of this.inMemoryEntries) {
212
- if (entry.type === "session") {
213
- return entry.id;
214
- }
215
- }
216
- return null;
217
67
  }
218
- loadEntriesFromFile() {
219
- if (!existsSync(this.contextFile))
220
- return [];
221
- const content = readFileSync(this.contextFile, "utf8");
222
- const entries = [];
223
- const lines = content.trim().split("\n");
224
- for (const line of lines) {
225
- if (!line.trim())
68
+ // Read log.jsonl and find user messages not in context
69
+ const logContent = readFileSync(logFile, "utf-8");
70
+ const logLines = logContent.trim().split("\n").filter(Boolean);
71
+ const newMessages = [];
72
+ for (const line of logLines) {
73
+ try {
74
+ const logMsg = JSON.parse(line);
75
+ const slackTs = logMsg.ts;
76
+ const date = logMsg.date;
77
+ if (!slackTs || !date)
226
78
  continue;
227
- try {
228
- const entry = JSON.parse(line);
229
- entries.push(entry);
230
- }
231
- catch {
232
- // Skip malformed lines
233
- }
79
+ // Skip the current message being processed (will be added via prompt())
80
+ if (excludeSlackTs && slackTs === excludeSlackTs)
81
+ continue;
82
+ // Skip bot messages - added through agent flow
83
+ if (logMsg.isBot)
84
+ continue;
85
+ // Build the message text as it would appear in context
86
+ const messageText = `[${logMsg.userName || logMsg.user || "unknown"}]: ${logMsg.text || ""}`;
87
+ // Skip if this exact message text is already in context
88
+ if (existingMessages.has(messageText))
89
+ continue;
90
+ const msgTime = new Date(date).getTime() || Date.now();
91
+ const userMessage = {
92
+ role: "user",
93
+ content: [{ type: "text", text: messageText }],
94
+ timestamp: msgTime,
95
+ };
96
+ newMessages.push({ timestamp: msgTime, message: userMessage });
97
+ existingMessages.add(messageText); // Track to avoid duplicates within this sync
234
98
  }
235
- return entries;
236
- }
237
- saveMessage(message) {
238
- const entry = { ...this._createEntryBase(), type: "message", message };
239
- this.inMemoryEntries.push(entry);
240
- this._persist(entry);
241
- }
242
- saveThinkingLevelChange(thinkingLevel) {
243
- const entry = {
244
- ...this._createEntryBase(),
245
- type: "thinking_level_change",
246
- thinkingLevel,
247
- };
248
- this.inMemoryEntries.push(entry);
249
- this._persist(entry);
250
- }
251
- saveModelChange(provider, modelId) {
252
- const entry = { ...this._createEntryBase(), type: "model_change", provider, modelId };
253
- this.inMemoryEntries.push(entry);
254
- this._persist(entry);
255
- }
256
- saveCompaction(entry) {
257
- this.inMemoryEntries.push(entry);
258
- this._persist(entry);
259
- }
260
- /** Load session with compaction support */
261
- buildSessionContex() {
262
- const entries = this.loadEntries();
263
- return buildSessionContext(entries);
264
- }
265
- loadEntries() {
266
- // Re-read from file to get latest state
267
- const entries = existsSync(this.contextFile) ? this.loadEntriesFromFile() : this.inMemoryEntries;
268
- return entries.filter((e) => e.type !== "session");
269
- }
270
- getSessionId() {
271
- return this.sessionId;
272
- }
273
- getSessionFile() {
274
- return this.contextFile;
275
- }
276
- /** Reset session (clears context.jsonl) */
277
- reset() {
278
- this.sessionId = uuidv4();
279
- this.flushed = false;
280
- this.inMemoryEntries = [
281
- {
282
- type: "session",
283
- id: this.sessionId,
284
- timestamp: new Date().toISOString(),
285
- cwd: this.channelDir,
286
- },
287
- ];
288
- // Truncate the context file
289
- if (existsSync(this.contextFile)) {
290
- writeFileSync(this.contextFile, "");
99
+ catch {
100
+ // Skip malformed lines
291
101
  }
292
102
  }
293
- // Compatibility methods for AgentSession
294
- isPersisted() {
295
- return true;
296
- }
297
- setSessionFile(_path) {
298
- // No-op for mom - we always use the channel's context.jsonl
299
- }
300
- loadModel() {
301
- return this.buildSessionContex().model;
302
- }
303
- loadThinkingLevel() {
304
- return this.buildSessionContex().thinkingLevel;
305
- }
306
- /** Not used by mom but required by AgentSession interface */
307
- createBranchedSession(_leafId) {
308
- return null; // Mom doesn't support branching
103
+ if (newMessages.length === 0)
104
+ return 0;
105
+ // Sort by timestamp and add to session
106
+ newMessages.sort((a, b) => a.timestamp - b.timestamp);
107
+ for (const { message } of newMessages) {
108
+ sessionManager.appendMessage(message);
309
109
  }
110
+ return newMessages.length;
310
111
  }
311
112
  const DEFAULT_COMPACTION = {
312
113
  enabled: true,
@@ -417,112 +218,4 @@ export class MomSettingsManager {
417
218
  return 30000;
418
219
  }
419
220
  }
420
- // ============================================================================
421
- // Sync log.jsonl to context.jsonl
422
- // ============================================================================
423
- /**
424
- * Sync user messages from log.jsonl to context.jsonl.
425
- *
426
- * This ensures that messages logged while mom wasn't running (channel chatter,
427
- * backfilled messages, messages while busy) are added to the LLM context.
428
- *
429
- * @param channelDir - Path to channel directory
430
- * @param excludeAfterTs - Don't sync messages with ts >= this value (they'll be handled by agent)
431
- * @returns Number of messages synced
432
- */
433
- export function syncLogToContext(channelDir, excludeAfterTs) {
434
- const logFile = join(channelDir, "log.jsonl");
435
- const contextFile = join(channelDir, "context.jsonl");
436
- if (!existsSync(logFile))
437
- return 0;
438
- // Read all user messages from log.jsonl
439
- const logContent = readFileSync(logFile, "utf-8");
440
- const logLines = logContent.trim().split("\n").filter(Boolean);
441
- const logMessages = [];
442
- for (const line of logLines) {
443
- try {
444
- const entry = JSON.parse(line);
445
- // Only sync user messages (not bot responses)
446
- if (!entry.isBot && entry.ts && entry.text) {
447
- // Skip if >= excludeAfterTs
448
- if (excludeAfterTs && entry.ts >= excludeAfterTs)
449
- continue;
450
- logMessages.push(entry);
451
- }
452
- }
453
- catch { }
454
- }
455
- if (logMessages.length === 0)
456
- return 0;
457
- // Read existing timestamps from context.jsonl
458
- if (existsSync(contextFile)) {
459
- const contextContent = readFileSync(contextFile, "utf-8");
460
- const contextLines = contextContent.trim().split("\n").filter(Boolean);
461
- for (const line of contextLines) {
462
- try {
463
- const entry = JSON.parse(line);
464
- if (entry.type === "message" && entry.message?.role === "user" && entry.message?.timestamp) {
465
- // Extract ts from timestamp (ms -> slack ts format for comparison)
466
- // We store the original slack ts in a way we can recover
467
- // Actually, let's just check by content match since ts formats differ
468
- }
469
- }
470
- catch { }
471
- }
472
- }
473
- // For deduplication, we need to track what's already in context
474
- // Read context and extract user message content (strip attachments section for comparison)
475
- const existingMessages = new Set();
476
- if (existsSync(contextFile)) {
477
- const contextContent = readFileSync(contextFile, "utf-8");
478
- const contextLines = contextContent.trim().split("\n").filter(Boolean);
479
- for (const line of contextLines) {
480
- try {
481
- const entry = JSON.parse(line);
482
- if (entry.type === "message" && entry.message?.role === "user") {
483
- let content = typeof entry.message.content === "string" ? entry.message.content : entry.message.content?.[0]?.text;
484
- if (content) {
485
- // Strip timestamp prefix for comparison (live messages have it, log messages don't)
486
- // Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text
487
- content = content.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\] /, "");
488
- // Strip attachments section for comparison (live messages have it, log messages don't)
489
- const attachmentsIdx = content.indexOf("\n\n<slack_attachments>\n");
490
- if (attachmentsIdx !== -1) {
491
- content = content.substring(0, attachmentsIdx);
492
- }
493
- existingMessages.add(content);
494
- }
495
- }
496
- }
497
- catch { }
498
- }
499
- }
500
- // Add missing messages to context.jsonl
501
- let syncedCount = 0;
502
- for (const msg of logMessages) {
503
- const userName = msg.userName || msg.user;
504
- const content = `[${userName}]: ${msg.text}`;
505
- // Skip if already in context
506
- if (existingMessages.has(content))
507
- continue;
508
- const timestamp = Math.floor(parseFloat(msg.ts) * 1000);
509
- const entry = {
510
- type: "message",
511
- timestamp: new Date(timestamp).toISOString(),
512
- message: {
513
- role: "user",
514
- content,
515
- timestamp,
516
- },
517
- };
518
- // Ensure directory exists
519
- if (!existsSync(channelDir)) {
520
- mkdirSync(channelDir, { recursive: true });
521
- }
522
- appendFileSync(contextFile, `${JSON.stringify(entry)}\n`);
523
- existingMessages.add(content); // Track to avoid duplicates within this sync
524
- syncedCount++;
525
- }
526
- return syncedCount;
527
- }
528
221
  //# sourceMappingURL=context.js.map
@@ -1 +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 { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\tbuildSessionContext,\n\ttype CompactionEntry,\n\ttype FileEntry,\n\ttype ModelChangeEntry,\n\ttype SessionContext,\n\ttype SessionEntry,\n\ttype SessionEntryBase,\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 flushed: boolean = false;\n\tprivate inMemoryEntries: FileEntry[] = [];\n\tprivate leafId: string | null = null;\n\n\tconstructor(channelDir: 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._updateLeafId();\n\t\t\tthis.flushed = true;\n\t\t} else {\n\t\t\tthis.sessionId = uuidv4();\n\t\t\tthis.inMemoryEntries = [\n\t\t\t\t{\n\t\t\t\t\ttype: \"session\",\n\t\t\t\t\tversion: 2,\n\t\t\t\t\tid: this.sessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tcwd: this.channelDir,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t\t// Note: syncFromLog() is called explicitly from agent.ts with excludeTimestamp\n\t}\n\n\tprivate _updateLeafId(): void {\n\t\tfor (let i = this.inMemoryEntries.length - 1; i >= 0; i--) {\n\t\t\tconst entry = this.inMemoryEntries[i];\n\t\t\tif (entry.type !== \"session\") {\n\t\t\t\tthis.leafId = entry.id;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.leafId = null;\n\t}\n\n\tprivate _createEntryBase(): Omit<SessionEntryBase, \"type\"> {\n\t\tconst id = uuidv4();\n\t\tconst base = {\n\t\t\tid,\n\t\t\tparentId: this.leafId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t};\n\t\tthis.leafId = id;\n\t\treturn base;\n\t}\n\n\tprivate _persist(entry: SessionEntry): void {\n\t\tconst hasAssistant = this.inMemoryEntries.some((e) => e.type === \"message\" && e.message.role === \"assistant\");\n\t\tif (!hasAssistant) return;\n\n\t\tif (!this.flushed) {\n\t\t\tfor (const e of this.inMemoryEntries) {\n\t\t\t\tappendFileSync(this.contextFile, `${JSON.stringify(e)}\\n`);\n\t\t\t}\n\t\t\tthis.flushed = true;\n\t\t} else {\n\t\t\tappendFileSync(this.contextFile, `${JSON.stringify(entry)}\\n`);\n\t\t}\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// AgentMessage 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: AgentMessage }> = [];\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: AgentMessage = {\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 id = uuidv4();\n\t\t\tconst entry: SessionMessageEntry = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tid,\n\t\t\t\tparentId: this.leafId,\n\t\t\t\ttimestamp, // Use log date as entry timestamp for consistent deduplication\n\t\t\t\tmessage,\n\t\t\t};\n\t\t\tthis.leafId = id;\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(): FileEntry[] {\n\t\tif (!existsSync(this.contextFile)) return [];\n\n\t\tconst content = readFileSync(this.contextFile, \"utf8\");\n\t\tconst entries: FileEntry[] = [];\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 FileEntry;\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\tsaveMessage(message: AgentMessage): void {\n\t\tconst entry: SessionMessageEntry = { ...this._createEntryBase(), type: \"message\", message };\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\t...this._createEntryBase(),\n\t\t\ttype: \"thinking_level_change\",\n\t\t\tthinkingLevel,\n\t\t};\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = { ...this._createEntryBase(), type: \"model_change\", provider, modelId };\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\t/** Load session with compaction support */\n\tbuildSessionContex(): SessionContext {\n\t\tconst entries = this.loadEntries();\n\t\treturn buildSessionContext(entries);\n\t}\n\n\tloadEntries(): SessionEntry[] {\n\t\t// Re-read from file to get latest state\n\t\tconst entries = existsSync(this.contextFile) ? this.loadEntriesFromFile() : this.inMemoryEntries;\n\t\treturn entries.filter((e): e is SessionEntry => e.type !== \"session\");\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/** Reset session (clears context.jsonl) */\n\treset(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tthis.flushed = false;\n\t\tthis.inMemoryEntries = [\n\t\t\t{\n\t\t\t\ttype: \"session\",\n\t\t\t\tid: this.sessionId,\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tcwd: this.channelDir,\n\t\t\t},\n\t\t];\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\tisPersisted(): 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.buildSessionContex().model;\n\t}\n\n\tloadThinkingLevel(): string {\n\t\treturn this.buildSessionContex().thinkingLevel;\n\t}\n\n\t/** Not used by mom but required by AgentSession interface */\n\tcreateBranchedSession(_leafId: string): 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\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn \"one-at-a-time\"; // Mom processes one message at a time\n\t}\n\n\tsetSteeringMode(_mode: \"all\" | \"one-at-a-time\"): void {\n\t\t// No-op for mom\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn \"one-at-a-time\"; // Mom processes one message at a time\n\t}\n\n\tsetFollowUpMode(_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\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 (strip attachments section for comparison)\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\tlet 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) {\n\t\t\t\t\t\t// Strip timestamp prefix for comparison (live messages have it, log messages don't)\n\t\t\t\t\t\t// Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text\n\t\t\t\t\t\tcontent = content.replace(/^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}\\] /, \"\");\n\t\t\t\t\t\t// Strip attachments section for comparison (live messages have it, log messages don't)\n\t\t\t\t\t\tconst attachmentsIdx = content.indexOf(\"\\n\\n<slack_attachments>\\n\");\n\t\t\t\t\t\tif (attachmentsIdx !== -1) {\n\t\t\t\t\t\t\tcontent = content.substring(0, attachmentsIdx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\texistingMessages.add(content);\n\t\t\t\t\t}\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"]}
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 * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager\n * - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)\n */\n\nimport type { UserMessage } from \"@mariozechner/pi-ai\";\nimport type { SessionManager, SessionMessageEntry } from \"@mariozechner/pi-coding-agent\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n// ============================================================================\n// Sync log.jsonl to SessionManager\n// ============================================================================\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tisBot?: boolean;\n}\n\n/**\n * Sync user messages from log.jsonl to SessionManager.\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 sessionManager - The SessionManager to sync to\n * @param channelDir - Path to channel directory containing log.jsonl\n * @param excludeSlackTs - Slack timestamp of current message (will be added via prompt(), not sync)\n * @returns Number of messages synced\n */\nexport function syncLogToSessionManager(\n\tsessionManager: SessionManager,\n\tchannelDir: string,\n\texcludeSlackTs?: string,\n): number {\n\tconst logFile = join(channelDir, \"log.jsonl\");\n\n\tif (!existsSync(logFile)) return 0;\n\n\t// Build set of existing message content from session\n\tconst existingMessages = new Set<string>();\n\tfor (const entry of sessionManager.getEntries()) {\n\t\tif (entry.type === \"message\") {\n\t\t\tconst msgEntry = entry as SessionMessageEntry;\n\t\t\tconst msg = msgEntry.message as { role: string; content?: unknown };\n\t\t\tif (msg.role === \"user\" && msg.content !== undefined) {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (typeof content === \"string\") {\n\t\t\t\t\t// Strip timestamp prefix for comparison (live messages have it, synced don't)\n\t\t\t\t\t// Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text\n\t\t\t\t\tlet normalized = content.replace(/^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}\\] /, \"\");\n\t\t\t\t\t// Strip attachments section\n\t\t\t\t\tconst attachmentsIdx = normalized.indexOf(\"\\n\\n<slack_attachments>\\n\");\n\t\t\t\t\tif (attachmentsIdx !== -1) {\n\t\t\t\t\t\tnormalized = normalized.substring(0, attachmentsIdx);\n\t\t\t\t\t}\n\t\t\t\t\texistingMessages.add(normalized);\n\t\t\t\t} else if (Array.isArray(content)) {\n\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\ttypeof part === \"object\" &&\n\t\t\t\t\t\t\tpart !== null &&\n\t\t\t\t\t\t\t\"type\" in part &&\n\t\t\t\t\t\t\tpart.type === \"text\" &&\n\t\t\t\t\t\t\t\"text\" in part\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tlet normalized = (part as { type: \"text\"; text: string }).text;\n\t\t\t\t\t\t\tnormalized = normalized.replace(/^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}\\] /, \"\");\n\t\t\t\t\t\t\tconst attachmentsIdx = normalized.indexOf(\"\\n\\n<slack_attachments>\\n\");\n\t\t\t\t\t\t\tif (attachmentsIdx !== -1) {\n\t\t\t\t\t\t\t\tnormalized = normalized.substring(0, attachmentsIdx);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\texistingMessages.add(normalized);\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\t}\n\n\t// Read log.jsonl and find user messages not in context\n\tconst logContent = readFileSync(logFile, \"utf-8\");\n\tconst logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n\tconst newMessages: Array<{ timestamp: number; message: UserMessage }> = [];\n\n\tfor (const line of logLines) {\n\t\ttry {\n\t\t\tconst logMsg: LogMessage = JSON.parse(line);\n\n\t\t\tconst slackTs = logMsg.ts;\n\t\t\tconst date = logMsg.date;\n\t\t\tif (!slackTs || !date) continue;\n\n\t\t\t// Skip the current message being processed (will be added via prompt())\n\t\t\tif (excludeSlackTs && slackTs === excludeSlackTs) continue;\n\n\t\t\t// Skip bot messages - added through agent flow\n\t\t\tif (logMsg.isBot) continue;\n\n\t\t\t// Build the message text as it would appear in context\n\t\t\tconst messageText = `[${logMsg.userName || logMsg.user || \"unknown\"}]: ${logMsg.text || \"\"}`;\n\n\t\t\t// Skip if this exact message text is already in context\n\t\t\tif (existingMessages.has(messageText)) continue;\n\n\t\t\tconst msgTime = new Date(date).getTime() || Date.now();\n\t\t\tconst userMessage: UserMessage = {\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [{ type: \"text\", text: messageText }],\n\t\t\t\ttimestamp: msgTime,\n\t\t\t};\n\n\t\t\tnewMessages.push({ timestamp: msgTime, message: userMessage });\n\t\t\texistingMessages.add(messageText); // Track to avoid duplicates within this sync\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\tif (newMessages.length === 0) return 0;\n\n\t// Sort by timestamp and add to session\n\tnewMessages.sort((a, b) => a.timestamp - b.timestamp);\n\n\tfor (const { message } of newMessages) {\n\t\tsessionManager.appendMessage(message);\n\t}\n\n\treturn newMessages.length;\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\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn \"one-at-a-time\"; // Mom processes one message at a time\n\t}\n\n\tsetSteeringMode(_mode: \"all\" | \"one-at-a-time\"): void {\n\t\t// No-op for mom\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn \"one-at-a-time\"; // Mom processes one message at a time\n\t}\n\n\tsetFollowUpMode(_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"]}