@mariozechner/pi-mom 0.27.4 → 0.27.6
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.d.ts.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/package.json +4 -4
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,aAAa,EAGlB,KAAK,YAAY,EAGjB,MAAM,+BAA+B,CAAC;AAiBvC;;;;;GAKG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAsB;IAE7C,YAAY,UAAU,EAAE,MAAM,EA2B7B;IAED,OAAO,CAAC,QAAQ;IAchB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CA2GzC;IAED,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,mBAAmB;IAoB3B,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAQrC;IAED,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAQnD;IAED,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CASvD;IAED,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAG3C;IAED,2CAA2C;IAC3C,WAAW,IAAI,aAAa,CAG3B;IAED,WAAW,IAAI,YAAY,EAAE,CAM5B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,2CAA2C;IAC3C,KAAK,IAAI,IAAI,CAeZ;IAGD,WAAW,IAAI,OAAO,CAErB;IAED,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAElC;IAED,SAAS,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAExD;IAED,iBAAiB,IAAI,MAAM,CAE1B;IAED,6DAA6D;IAC7D,gCAAgC,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpG;CACD;AAMD,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAClC;AAcD;;;GAGG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAc;IAE9B,YAAY,YAAY,EAAE,MAAM,EAG/B;IAED,OAAO,CAAC,IAAI;IAaZ,OAAO,CAAC,IAAI;IAYZ,qBAAqB,IAAI,qBAAqB,CAK7C;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAG3C;IAED,gBAAgB,IAAI,gBAAgB,CAKnC;IAED,eAAe,IAAI,OAAO,CAEzB;IAED,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAGtC;IAED,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvC;IAED,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAIlE;IAED,uBAAuB,IAAI,MAAM,CAEhC;IAED,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAG3C;IAGD,YAAY,IAAI,KAAK,GAAG,eAAe,CAEtC;IAED,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAEjD;IAED,YAAY,IAAI,MAAM,EAAE,CAEvB;IAED,cAAc,IAAI,MAAM,CAEvB;CACD;AAMD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CA4GpF","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 { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\ttype CompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\ttype SessionEntry,\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: SessionEntry[] = [];\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.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\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 _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// 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\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\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\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\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 = {\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\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\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/** 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.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\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.d.ts","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 { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\tbuildSessionContext,\n\ttype CompactionEntry,\n\ttype LoadedSession,\n\ttype ModelChangeEntry,\n\ttype SessionEntry,\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: SessionEntry[] = [];\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.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\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 _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// 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\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\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\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\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 = {\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\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\tloadSession(): LoadedSession {\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\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/** 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.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\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"]}
|
package/dist/context.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - MomSessionManager: Adapts coding-agent's SessionManager for channel-based storage
|
|
10
10
|
* - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { buildSessionContext, } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
import { randomBytes } from "crypto";
|
|
14
14
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
15
15
|
import { dirname, join } from "path";
|
|
@@ -242,7 +242,7 @@ export class MomSessionManager {
|
|
|
242
242
|
/** Load session with compaction support */
|
|
243
243
|
loadSession() {
|
|
244
244
|
const entries = this.loadEntries();
|
|
245
|
-
return
|
|
245
|
+
return buildSessionContext(entries);
|
|
246
246
|
}
|
|
247
247
|
loadEntries() {
|
|
248
248
|
// Re-read from file to get latest state
|
package/dist/context.js.map
CHANGED
|
@@ -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 { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\ttype CompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\ttype SessionEntry,\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: SessionEntry[] = [];\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.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\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 _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// 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\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\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\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\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 = {\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\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\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/** 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.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\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 * - MomSessionManager: Adapts coding-agent's SessionManager for channel-based storage\n * - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport {\n\tbuildSessionContext,\n\ttype CompactionEntry,\n\ttype LoadedSession,\n\ttype ModelChangeEntry,\n\ttype SessionEntry,\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: SessionEntry[] = [];\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.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\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 _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// 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\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\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\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\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 = {\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\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\tloadSession(): LoadedSession {\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\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/** 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.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\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-mom",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.6",
|
|
4
4
|
"description": "Slack bot that delegates messages to the pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
|
24
|
-
"@mariozechner/pi-agent-core": "^0.27.
|
|
25
|
-
"@mariozechner/pi-ai": "^0.27.
|
|
26
|
-
"@mariozechner/pi-coding-agent": "^0.27.
|
|
24
|
+
"@mariozechner/pi-agent-core": "^0.27.6",
|
|
25
|
+
"@mariozechner/pi-ai": "^0.27.6",
|
|
26
|
+
"@mariozechner/pi-coding-agent": "^0.27.6",
|
|
27
27
|
"@sinclair/typebox": "^0.34.0",
|
|
28
28
|
"@slack/socket-mode": "^2.0.0",
|
|
29
29
|
"@slack/web-api": "^7.0.0",
|