Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { LcmContextEngine } from "./src/engine.js";
import { createLcmDescribeTool } from "./src/tools/lcm-describe-tool.js";
import { createLcmExpandQueryTool } from "./src/tools/lcm-expand-query-tool.js";
import { createLcmExpandTool } from "./src/tools/lcm-expand-tool.js";
import { createLcmExportTool } from "./src/tools/lcm-export-tool.js";
import { createLcmGrepTool } from "./src/tools/lcm-grep-tool.js";
import { createLcmUpdatePeerTool } from "./src/tools/lcm-update-peer-tool.js";
import type { LcmDependencies } from "./src/types.js";

/** Parse `agent:<agentId>:<suffix...>` session keys. */
Expand Down Expand Up @@ -1315,6 +1317,62 @@ const lcmPlugin = {
requesterSessionKey: ctx.sessionKey,
}),
);
api.registerTool((ctx) =>
createLcmExportTool({
deps,
}),
);
api.registerTool((ctx) =>
createLcmUpdatePeerTool({
deps,
lcm,
sessionId: ctx.sessionId,
sessionKey: ctx.sessionKey,
}),
);

// Register hook to auto-extract peer info from inbound messages
api.registerHook(["message_received"], async (event) => {
const meta = event.context?.inboundMeta;
if (!meta) return;

const sessionId = event.sessionId;
if (!sessionId) return;

// Extract peer info from inbound metadata
const chatId = meta.chat_id;
const channel = meta.channel;
const chatType = meta.chat_type;

// For DMs, the peer is the sender
// For groups, the peer is the chat/group
let peerId: string | undefined;
let peerName: string | undefined;

if (chatType === "dm" && meta.sender_id) {
peerId = `user:${meta.sender_id}`;
peerName = meta.sender_name || meta.sender;
} else if (chatType === "group" && chatId) {
peerId = chatId;
peerName = meta.chat_name || meta.group_name;
}

if (!peerId) return;

// Update conversation with peer info
try {
await lcm.updateConversationPeer({
sessionId,
peerId,
peerName,
channel,
chatType,
});
deps.log.info(`[lcm] Auto-extracted peer: ${peerId} (${peerName || "unknown"}) for session ${sessionId}`);
} catch (error) {
deps.log.warn(`[lcm] Failed to update peer info: ${error}`);
}
});

api.logger.info(
`[lcm] Plugin loaded (enabled=${deps.config.enabled}, db=${deps.config.databasePath}, threshold=${deps.config.contextThreshold})`,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/db/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,17 @@ export function runLcmMigrations(
options?: { fts5Available?: boolean },
): void {
db.exec(`
CREATE TABLE IF NOT EXISTS contacts (
peer_id TEXT PRIMARY KEY,
peer_name TEXT,
chat_type TEXT CHECK (chat_type IN ('dm', 'group'))
);

CREATE TABLE IF NOT EXISTS conversations (
conversation_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
peer_id TEXT REFERENCES contacts(peer_id) ON DELETE SET NULL,
channel TEXT,
title TEXT,
bootstrapped_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
Expand Down Expand Up @@ -491,6 +499,30 @@ export function runLcmMigrations(
db.exec(`ALTER TABLE conversations ADD COLUMN bootstrapped_at TEXT`);
}

// Add peer_id and channel columns to conversations for existing DBs
const hasPeerId = conversationColumns.some((col) => col.name === "peer_id");
const hasChannel = conversationColumns.some((col) => col.name === "channel");
if (!hasPeerId) {
db.exec(`ALTER TABLE conversations ADD COLUMN peer_id TEXT REFERENCES contacts(peer_id) ON DELETE SET NULL`);
}
if (!hasChannel) {
db.exec(`ALTER TABLE conversations ADD COLUMN channel TEXT`);
}

// Ensure contacts table exists for existing DBs
const contactsTable = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='contacts'")
.get();
if (!contactsTable) {
db.exec(`
CREATE TABLE contacts (
peer_id TEXT PRIMARY KEY,
peer_name TEXT,
chat_type TEXT CHECK (chat_type IN ('dm', 'group'))
)
`);
}

ensureSummaryDepthColumn(db);
ensureSummaryMetadataColumns(db);
backfillSummaryDepths(db);
Expand Down
54 changes: 54 additions & 0 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type MessagePartType,
} from "./store/conversation-store.js";
import { SummaryStore } from "./store/summary-store.js";
import { ContactStore, type CreateContactInput } from "./store/contact-store.js";
import { createLcmSummarizeFromLegacyParams } from "./summarize.js";
import type { LcmDependencies } from "./types.js";

Expand Down Expand Up @@ -588,6 +589,7 @@ export class LcmContextEngine implements ContextEngine {

private conversationStore: ConversationStore;
private summaryStore: SummaryStore;
private contactStore: ContactStore;
private assembler: ContextAssembler;
private compaction: CompactionEngine;
private retrieval: RetrievalEngine;
Expand All @@ -607,6 +609,7 @@ export class LcmContextEngine implements ContextEngine {

this.conversationStore = new ConversationStore(db, { fts5Available: this.fts5Available });
this.summaryStore = new SummaryStore(db, { fts5Available: this.fts5Available });
this.contactStore = new ContactStore(db);

if (!this.fts5Available) {
this.deps.log.warn(
Expand Down Expand Up @@ -1219,6 +1222,57 @@ export class LcmContextEngine implements ContextEngine {
});
}

/**
* Update conversation with peer information.
* Creates or updates the contact and associates it with the conversation.
*
* @param sessionId - The session ID
* @param peerInfo - Peer information (peerId, peerName, channel, chatType)
*/
async updateConversationPeer(params: {
sessionId: string;
peerId: string;
peerName?: string;
channel?: string;
chatType?: "dm" | "group";
}): Promise<void> {
this.ensureMigrated();

const { sessionId, peerId, peerName, channel, chatType } = params;

// Create or update contact
this.contactStore.create({
peerId,
peerName,
chatType,
});

// Update conversation with peer info
const conversation = await this.conversationStore.getConversationBySessionId(sessionId);
if (conversation) {
// Only update if not already set
if (!conversation.peerId || !conversation.channel) {
const db = getLcmConnection(this.config.databasePath);
const updates: string[] = [];
const values: (string | null)[] = [];

if (!conversation.peerId) {
updates.push("peer_id = ?");
values.push(peerId);
}
if (!conversation.channel && channel) {
updates.push("channel = ?");
values.push(channel);
}

if (updates.length > 0) {
values.push(String(conversation.conversationId));
db.prepare(`UPDATE conversations SET ${updates.join(", ")} WHERE conversation_id = ?`).run(...values);
}
}
}
}

async afterTurn(params: {
sessionId: string;
sessionFile: string;
Expand Down
Loading