mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 18:03:50 +00:00
Removes MomSessionManager which was missing methods expected by AgentSession (appendMessage, getBranch, etc.), causing runtime crashes. Now uses the standard SessionManager from coding-agent with a fixed context.jsonl path per channel. The syncLogToSessionManager function handles syncing messages from log.jsonl using SessionManager's public API. Ref #595
297 lines
8.3 KiB
TypeScript
297 lines
8.3 KiB
TypeScript
/**
|
|
* Context management for mom.
|
|
*
|
|
* Mom uses two files per channel:
|
|
* - context.jsonl: Structured API messages for LLM context (same format as coding-agent sessions)
|
|
* - log.jsonl: Human-readable channel history for grep (no tool results)
|
|
*
|
|
* This module provides:
|
|
* - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager
|
|
* - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences)
|
|
*/
|
|
|
|
import type { UserMessage } from "@mariozechner/pi-ai";
|
|
import type { SessionManager, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
import { dirname, join } from "path";
|
|
|
|
// ============================================================================
|
|
// Sync log.jsonl to SessionManager
|
|
// ============================================================================
|
|
|
|
interface LogMessage {
|
|
date?: string;
|
|
ts?: string;
|
|
user?: string;
|
|
userName?: string;
|
|
text?: string;
|
|
isBot?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Sync user messages from log.jsonl to SessionManager.
|
|
*
|
|
* This ensures that messages logged while mom wasn't running (channel chatter,
|
|
* backfilled messages, messages while busy) are added to the LLM context.
|
|
*
|
|
* @param sessionManager - The SessionManager to sync to
|
|
* @param channelDir - Path to channel directory containing log.jsonl
|
|
* @param excludeSlackTs - Slack timestamp of current message (will be added via prompt(), not sync)
|
|
* @returns Number of messages synced
|
|
*/
|
|
export function syncLogToSessionManager(
|
|
sessionManager: SessionManager,
|
|
channelDir: string,
|
|
excludeSlackTs?: string,
|
|
): number {
|
|
const logFile = join(channelDir, "log.jsonl");
|
|
|
|
if (!existsSync(logFile)) return 0;
|
|
|
|
// Build set of existing message content from session
|
|
const existingMessages = new Set<string>();
|
|
for (const entry of sessionManager.getEntries()) {
|
|
if (entry.type === "message") {
|
|
const msgEntry = entry as SessionMessageEntry;
|
|
const msg = msgEntry.message as { role: string; content?: unknown };
|
|
if (msg.role === "user" && msg.content !== undefined) {
|
|
const content = msg.content;
|
|
if (typeof content === "string") {
|
|
// Strip timestamp prefix for comparison (live messages have it, synced don't)
|
|
// Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text
|
|
let normalized = content.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\] /, "");
|
|
// Strip attachments section
|
|
const attachmentsIdx = normalized.indexOf("\n\n<slack_attachments>\n");
|
|
if (attachmentsIdx !== -1) {
|
|
normalized = normalized.substring(0, attachmentsIdx);
|
|
}
|
|
existingMessages.add(normalized);
|
|
} else if (Array.isArray(content)) {
|
|
for (const part of content) {
|
|
if (
|
|
typeof part === "object" &&
|
|
part !== null &&
|
|
"type" in part &&
|
|
part.type === "text" &&
|
|
"text" in part
|
|
) {
|
|
let normalized = (part as { type: "text"; text: string }).text;
|
|
normalized = normalized.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\] /, "");
|
|
const attachmentsIdx = normalized.indexOf("\n\n<slack_attachments>\n");
|
|
if (attachmentsIdx !== -1) {
|
|
normalized = normalized.substring(0, attachmentsIdx);
|
|
}
|
|
existingMessages.add(normalized);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read log.jsonl and find user messages not in context
|
|
const logContent = readFileSync(logFile, "utf-8");
|
|
const logLines = logContent.trim().split("\n").filter(Boolean);
|
|
|
|
const newMessages: Array<{ timestamp: number; message: UserMessage }> = [];
|
|
|
|
for (const line of logLines) {
|
|
try {
|
|
const logMsg: LogMessage = JSON.parse(line);
|
|
|
|
const slackTs = logMsg.ts;
|
|
const date = logMsg.date;
|
|
if (!slackTs || !date) continue;
|
|
|
|
// Skip the current message being processed (will be added via prompt())
|
|
if (excludeSlackTs && slackTs === excludeSlackTs) continue;
|
|
|
|
// Skip bot messages - added through agent flow
|
|
if (logMsg.isBot) continue;
|
|
|
|
// Build the message text as it would appear in context
|
|
const messageText = `[${logMsg.userName || logMsg.user || "unknown"}]: ${logMsg.text || ""}`;
|
|
|
|
// Skip if this exact message text is already in context
|
|
if (existingMessages.has(messageText)) continue;
|
|
|
|
const msgTime = new Date(date).getTime() || Date.now();
|
|
const userMessage: UserMessage = {
|
|
role: "user",
|
|
content: [{ type: "text", text: messageText }],
|
|
timestamp: msgTime,
|
|
};
|
|
|
|
newMessages.push({ timestamp: msgTime, message: userMessage });
|
|
existingMessages.add(messageText); // Track to avoid duplicates within this sync
|
|
} catch {
|
|
// Skip malformed lines
|
|
}
|
|
}
|
|
|
|
if (newMessages.length === 0) return 0;
|
|
|
|
// Sort by timestamp and add to session
|
|
newMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
for (const { message } of newMessages) {
|
|
sessionManager.appendMessage(message);
|
|
}
|
|
|
|
return newMessages.length;
|
|
}
|
|
|
|
// ============================================================================
|
|
// MomSettingsManager - Simple settings for mom
|
|
// ============================================================================
|
|
|
|
export interface MomCompactionSettings {
|
|
enabled: boolean;
|
|
reserveTokens: number;
|
|
keepRecentTokens: number;
|
|
}
|
|
|
|
export interface MomRetrySettings {
|
|
enabled: boolean;
|
|
maxRetries: number;
|
|
baseDelayMs: number;
|
|
}
|
|
|
|
export interface MomSettings {
|
|
defaultProvider?: string;
|
|
defaultModel?: string;
|
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high";
|
|
compaction?: Partial<MomCompactionSettings>;
|
|
retry?: Partial<MomRetrySettings>;
|
|
}
|
|
|
|
const DEFAULT_COMPACTION: MomCompactionSettings = {
|
|
enabled: true,
|
|
reserveTokens: 16384,
|
|
keepRecentTokens: 20000,
|
|
};
|
|
|
|
const DEFAULT_RETRY: MomRetrySettings = {
|
|
enabled: true,
|
|
maxRetries: 3,
|
|
baseDelayMs: 2000,
|
|
};
|
|
|
|
/**
|
|
* Settings manager for mom.
|
|
* Stores settings in the workspace root directory.
|
|
*/
|
|
export class MomSettingsManager {
|
|
private settingsPath: string;
|
|
private settings: MomSettings;
|
|
|
|
constructor(workspaceDir: string) {
|
|
this.settingsPath = join(workspaceDir, "settings.json");
|
|
this.settings = this.load();
|
|
}
|
|
|
|
private load(): MomSettings {
|
|
if (!existsSync(this.settingsPath)) {
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(this.settingsPath, "utf-8");
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
private save(): void {
|
|
try {
|
|
const dir = dirname(this.settingsPath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
|
|
} catch (error) {
|
|
console.error(`Warning: Could not save settings file: ${error}`);
|
|
}
|
|
}
|
|
|
|
getCompactionSettings(): MomCompactionSettings {
|
|
return {
|
|
...DEFAULT_COMPACTION,
|
|
...this.settings.compaction,
|
|
};
|
|
}
|
|
|
|
getCompactionEnabled(): boolean {
|
|
return this.settings.compaction?.enabled ?? DEFAULT_COMPACTION.enabled;
|
|
}
|
|
|
|
setCompactionEnabled(enabled: boolean): void {
|
|
this.settings.compaction = { ...this.settings.compaction, enabled };
|
|
this.save();
|
|
}
|
|
|
|
getRetrySettings(): MomRetrySettings {
|
|
return {
|
|
...DEFAULT_RETRY,
|
|
...this.settings.retry,
|
|
};
|
|
}
|
|
|
|
getRetryEnabled(): boolean {
|
|
return this.settings.retry?.enabled ?? DEFAULT_RETRY.enabled;
|
|
}
|
|
|
|
setRetryEnabled(enabled: boolean): void {
|
|
this.settings.retry = { ...this.settings.retry, enabled };
|
|
this.save();
|
|
}
|
|
|
|
getDefaultModel(): string | undefined {
|
|
return this.settings.defaultModel;
|
|
}
|
|
|
|
getDefaultProvider(): string | undefined {
|
|
return this.settings.defaultProvider;
|
|
}
|
|
|
|
setDefaultModelAndProvider(provider: string, modelId: string): void {
|
|
this.settings.defaultProvider = provider;
|
|
this.settings.defaultModel = modelId;
|
|
this.save();
|
|
}
|
|
|
|
getDefaultThinkingLevel(): string {
|
|
return this.settings.defaultThinkingLevel || "off";
|
|
}
|
|
|
|
setDefaultThinkingLevel(level: string): void {
|
|
this.settings.defaultThinkingLevel = level as MomSettings["defaultThinkingLevel"];
|
|
this.save();
|
|
}
|
|
|
|
// Compatibility methods for AgentSession
|
|
getSteeringMode(): "all" | "one-at-a-time" {
|
|
return "one-at-a-time"; // Mom processes one message at a time
|
|
}
|
|
|
|
setSteeringMode(_mode: "all" | "one-at-a-time"): void {
|
|
// No-op for mom
|
|
}
|
|
|
|
getFollowUpMode(): "all" | "one-at-a-time" {
|
|
return "one-at-a-time"; // Mom processes one message at a time
|
|
}
|
|
|
|
setFollowUpMode(_mode: "all" | "one-at-a-time"): void {
|
|
// No-op for mom
|
|
}
|
|
|
|
getHookPaths(): string[] {
|
|
return []; // Mom doesn't use hooks
|
|
}
|
|
|
|
getHookTimeout(): number {
|
|
return 30000;
|
|
}
|
|
}
|