Skip to content
Merged
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
2 changes: 1 addition & 1 deletion plugins/codex-lcm/skills/lcm-recall/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: lcm-recall
description: Use when a task may depend on prior Codex session context, long-running work, compaction recovery, projectless memory, or a user asks what happened before.
description: Recover or resume Codex work after compaction, interruption, or handoff with lcm_pack_context; also use for prior session context, long-running work, projectless memory, or questions about what happened before.
---

# LCM Recall
Expand Down
61 changes: 61 additions & 0 deletions plugins/codex-lcm/src/event-codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { NormalizedEvent } from "./events.ts";

export class PersistedEventError extends Error {
constructor() {
super("Persisted event does not match schema version 1.");
this.name = "PersistedEventError";
}
}

export function decodePersistedEvent(serialized: string): NormalizedEvent {
const event = parsePersistedEvent(serialized);
if (!event) throw new PersistedEventError();
return event;
}

export function parsePersistedEvent(serialized: string): NormalizedEvent | undefined {
let value: unknown;
try {
value = JSON.parse(serialized);
} catch (error) {
if (error instanceof SyntaxError) return undefined;
throw error;
}
return isNormalizedEvent(value) ? value : undefined;
}

function isNormalizedEvent(value: unknown): value is NormalizedEvent {
if (!isRecord(value)) return false;
return value.schema_version === 1
&& isString(value.event_id)
&& isString(value.timestamp)
&& isString(value.hook_event)
&& isString(value.session_id)
&& isString(value.cwd)
&& isOptionalString(value.project)
&& isOptionalString(value.repo_root)
&& isOptionalString(value.git_branch)
&& isOptionalString(value.tool_name)
&& isRecord(value.payload)
&& Array.isArray(value.redactions)
&& Array.isArray(value.truncations)
&& isString(value.raw_input_sha256)
&& isNonNegativeNumber(value.original_bytes)
&& isNonNegativeNumber(value.sanitized_bytes);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isString(value: unknown): value is string {
return typeof value === "string";
}

function isOptionalString(value: unknown): value is string | undefined {
return value === undefined || isString(value);
}

function isNonNegativeNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
266 changes: 266 additions & 0 deletions plugins/codex-lcm/src/mcp-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// allow: SIZE_OK — declarative MCP tool schema table.
export const TOOLS = [
{
name: "lcm_health",
title: "LCM Health",
description: "Report Codex LCM storage and index health.",
inputSchema: { type: "object", properties: {} },
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_stats",
title: "LCM Stats",
description: "Report aggregate LCM index shape, summary depths, graph counts, and freshness without raw transcript text.",
inputSchema: { type: "object", properties: {} },
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_list_sessions",
title: "LCM List Sessions",
description: "List sessions across projects with time, root/child, pagination, and optional compact summary filters.",
inputSchema: {
type: "object",
properties: {
since: { type: "string" },
until: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
parentSessionId: { type: "string" },
rootsOnly: { type: "boolean", default: false },
includeSummaries: { type: "boolean", default: false, description: "Include compact titles, overviews, prompts, outcomes, and topics in this one response." },
limit: { type: "number", default: 50 },
cursor: { type: "string" },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_usage",
title: "LCM Usage",
description: "Aggregate captured Codex token usage across filtered sessions. With rootsOnly, selected roots include all descendant sessions.",
inputSchema: {
type: "object",
properties: {
since: { type: "string" },
until: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
parentSessionId: { type: "string" },
rootsOnly: { type: "boolean", default: false },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_grep",
title: "LCM Grep",
description: "Preferred standard workflow step 1 (grep): find relevant sessions in indexed memory, or search bounded sanitized overflow payloads when contentScope is overflow or both. Codex may surface this tool as mcp__codex_lcm__lcm_grep.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
limit: { type: "number", default: 10 },
cwd: { type: "string" },
repoRoot: { type: "string" },
contentScope: { type: "string", enum: ["memory", "overflow", "both"], default: "memory" },
excludeCurrentSession: { type: "boolean", default: false },
excludeSessionIds: { type: "array", items: { type: "string" } },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_describe",
title: "LCM Describe",
description: "Preferred standard workflow step 2 (describe): inspect a session, summary node, or file reference, including summary-node depth and source lineage metadata. Codex may surface this tool as mcp__codex_lcm__lcm_describe.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
nodeId: { type: "string" },
fileId: { type: "string" },
limit: { type: "number", default: 50 },
offset: { type: "number", default: 0, description: "Byte offset when reading an overflow reference." },
maxBytes: { type: "number", minimum: 4, maximum: 524288, default: 65536, description: "Maximum bytes to read from an overflow reference." },
includeLineage: { type: "boolean", default: false, description: "Include full source ID arrays instead of compact source counts." },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_expand",
title: "LCM Expand",
description: "Preferred standard workflow step 3 (expand): expand one summary node into bounded source summary nodes and high-signal source events. Codex may surface this tool as mcp__codex_lcm__lcm_expand.",
inputSchema: {
type: "object",
properties: {
nodeId: { type: "string" },
query: { type: "string" },
limit: { type: "number", default: 4 },
},
required: ["nodeId"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_expand_query",
title: "LCM Expand Query",
description: "Find matching summary nodes and recursively expand their source lineage into bounded evidence for a focused query.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
sessionIds: { type: "array", items: { type: "string" } },
budgetTokens: { type: "number", default: 2000 },
limit: { type: "number", default: 4 },
sourceLimit: { type: "number", default: 6, description: "Maximum source events or source summary nodes considered per matched node." },
overview: { type: "boolean", default: false, description: "Prefer higher-depth, source-rich summary nodes for broad overview queries." },
},
required: ["query"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_context_plan",
title: "LCM Context Plan",
description: "Estimate recent-session token pressure and report compaction proximity. Returns diagnostics only; Codex owns compaction.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
modelContextWindow: { type: "number", default: 128000 },
autoCompactTokenLimit: { type: "number", default: 96000 },
recentEventLimit: { type: "number", default: 80 },
canControlCompaction: {
type: "boolean",
const: false,
default: false,
description: "Always false; this tool reports context pressure but cannot own Codex compaction.",
},
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_current_session",
title: "LCM Current Session",
description: "Find the current or latest known session by session ID, cwd, or repo root.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_search_sessions",
title: "LCM Search Sessions",
description: "Search across Codex sessions with SQLite FTS.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
limit: { type: "number", default: 10 },
cwd: { type: "string" },
repoRoot: { type: "string" },
excludeCurrentSession: { type: "boolean", default: false },
excludeSessionIds: { type: "array", items: { type: "string" } },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_get_session",
title: "LCM Get Session",
description: "Retrieve sanitized raw events for a session, optionally paged for long sessions.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
limit: { type: "number" },
cursor: { type: "string" },
},
required: ["sessionId"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_get_session_summary",
title: "LCM Get Session Summary",
description: "Retrieve the deterministic extractive summary for a session, including topics and source event pointers.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
},
required: ["sessionId"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_get_session_graph",
title: "LCM Get Session Graph",
description: "Retrieve a bounded DAG slice for a session, including session, turn, event, checkpoint, and summary nodes.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
limit: { type: "number", default: 200 },
},
required: ["sessionId"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_get_recent_context",
title: "LCM Get Recent Context",
description: "Retrieve recent events for a session or latest cwd-matching session.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
cwd: { type: "string" },
repoRoot: { type: "string" },
limit: { type: "number", default: 20 },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_pack_context",
title: "LCM Pack Context",
description: "Recover and resume prior Codex work after compaction, interruption, or handoff. Returns model-ready, token-budgeted Markdown from relevant session summaries and source evidence.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
sessionIds: { type: "array", items: { type: "string" } },
budgetTokens: { type: "number", default: 1200 },
cwd: { type: "string" },
},
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
{
name: "lcm_record_note",
title: "LCM Record Note",
description: "Record a user-authored note as a first-class LCM event.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
cwd: { type: "string" },
text: { type: "string" },
},
required: ["sessionId", "cwd", "text"],
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
},
];
48 changes: 48 additions & 0 deletions plugins/codex-lcm/src/mcp-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { LcmDescription, SummaryNode } from "./storage.ts";

export function toolResult(text: string, structuredContent: unknown) {
return {
content: [{ type: "text", text }],
structuredContent,
};
}

export function withoutMarkdown<T extends { markdown: string }>(value: T): Omit<T, "markdown"> {
const { markdown: _markdown, ...rest } = value;
return rest;
}

export function compactDescription(description: LcmDescription): unknown {
if (description.target === "file_ref" || description.target === "overflow_ref") return description;
if (description.target === "summary_node") {
return {
...description,
node: compactSummaryNode(description.node),
source_nodes: description.source_nodes.map(compactSummaryNode),
};
}
return {
...description,
summary: description.summary ? compactSessionSummary(description.summary) : undefined,
summary_nodes: description.summary_nodes.map(compactSummaryNode),
};
}

function compactSummaryNode(node: SummaryNode): Omit<SummaryNode, "source_ids" | "source_event_ids"> & {
readonly source_count: number;
readonly source_event_count: number;
} {
const { source_ids: sourceIds, source_event_ids: sourceEventIds, ...rest } = node;
return {
...rest,
source_count: sourceIds.length,
source_event_count: sourceEventIds.length,
};
}

function compactSessionSummary<T extends { source_event_ids: string[] }>(
summary: T,
): Omit<T, "source_event_ids"> & { readonly source_event_count: number } {
const { source_event_ids: sourceEventIds, ...rest } = summary;
return { ...rest, source_event_count: sourceEventIds.length };
}
Loading