Skip to content
Closed
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
29 changes: 29 additions & 0 deletions codex-cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { createInputItem } from "./utils/input-utils";
import { initLogger } from "./utils/logger/log";
import { isModelSupportedForResponses } from "./utils/model-utils.js";
import { parseToolCall } from "./utils/parsers";
import { clearSessionMemory, sessionMemoryStatus } from "./utils/storage/session-memory.js";
import { onExit, setInkRenderer } from "./utils/terminal";
import chalk from "chalk";
import { spawnSync } from "child_process";
Expand All @@ -69,6 +70,7 @@ const cli = meow(
Usage
$ codex [options] <prompt>
$ codex completion <bash|zsh|fish>
$ codex memory <clear|status>

Options
--version Print version and exit
Expand Down Expand Up @@ -285,6 +287,33 @@ let config = loadConfig(undefined, undefined, {
isFullContext: fullContextMode,
});

// Project-local session memory commands
if (cli.input[0] === "memory") {
const cmd = cli.input[1];
if (cmd === "clear") {
clearSessionMemory();
// eslint-disable-next-line no-console
console.log(`Session memory cleared for project at ${process.cwd()}`);
process.exit(0);
}
if (cmd === "status") {
const status = sessionMemoryStatus();
if (!status.exists) {
// eslint-disable-next-line no-console
console.log(`No session memory found for project at ${process.cwd()}`);
} else {
// eslint-disable-next-line no-console
console.log(
`Session memory loaded: id=${status.session?.id} items=${status.itemsCount}`,
);
}
process.exit(0);
}
// eslint-disable-next-line no-console
console.error("Usage: codex memory <clear|status>");
process.exit(1);
}

// `prompt` can be updated later when the user resumes a previous session
// via the `--history` flag. Therefore it must be declared with `let` rather
// than `const`.
Expand Down
8 changes: 8 additions & 0 deletions codex-cli/src/components/chat/terminal-chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function TerminalChatInput({
openHelpOverlay,
openDiffOverlay,
openSessionsOverlay,
openMemoryOverlay,
onCompact,
interruptAgent,
active,
Expand All @@ -79,8 +80,10 @@ export default function TerminalChatInput({
openHelpOverlay: () => void;
openDiffOverlay: () => void;
openSessionsOverlay: () => void;
openMemoryOverlay: () => void;
onCompact: () => void;
interruptAgent: () => void;
onMemoryCommand?: (command: string) => void;
active: boolean;
thinkingSeconds: number;
// New: current conversation items so we can include them in bug reports
Expand Down Expand Up @@ -497,6 +500,10 @@ export default function TerminalChatInput({
setInput("");
openHelpOverlay();
return;
} else if (inputValue === "/memory") {
setInput("");
openMemoryOverlay();
return;
} else if (inputValue === "/diff") {
setInput("");
openDiffOverlay();
Expand Down Expand Up @@ -738,6 +745,7 @@ export default function TerminalChatInput({
openHelpOverlay,
openDiffOverlay,
openSessionsOverlay,
openMemoryOverlay,
history,
onCompact,
skipNextSubmit,
Expand Down
122 changes: 119 additions & 3 deletions codex-cli/src/components/chat/terminal-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ import {
import { createOpenAIClient } from "../../utils/openai-client.js";
import { shortCwd } from "../../utils/short-path.js";
import { saveRollout } from "../../utils/storage/save-rollout.js";
import { loadSessionMemory, saveSessionMemory } from "../../utils/storage/session-memory.js";
import { CLI_VERSION } from "../../version.js";
import ApprovalModeOverlay from "../approval-mode-overlay.js";
import DiffOverlay from "../diff-overlay.js";
import HelpOverlay from "../help-overlay.js";
import HistoryOverlay from "../history-overlay.js";
import MemoryOverlay from "../memory-overlay.js";
import ModelOverlay from "../model-overlay.js";
import SessionsOverlay from "../sessions-overlay.js";
import chalk from "chalk";
import fs from "fs/promises";
import { Box, Text } from "ink";
import { spawn } from "node:child_process";
import os from "os";
import path from "path";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { inspect } from "util";

Expand All @@ -49,7 +53,8 @@ export type OverlayModeType =
| "model"
| "approval"
| "help"
| "diff";
| "diff"
| "memory";

type Props = {
config: AppConfig;
Expand Down Expand Up @@ -148,7 +153,12 @@ export default function TerminalChat({
const [model, setModel] = useState<string>(config.model);
const [provider, setProvider] = useState<string>(config.provider || "openai");
const [lastResponseId, setLastResponseId] = useState<string | null>(null);
const [items, setItems] = useState<Array<ResponseItem>>([]);
const [memoryEnabled, setMemoryEnabled] = useState<boolean>(
Boolean(config.memory?.enabled),
);
const memory = memoryEnabled ? loadSessionMemory() : null;
const [sessionId] = useState<string>(() => memory?.session.id ?? crypto.randomUUID());
const [items, setItems] = useState<Array<ResponseItem>>(memory?.items ?? []);
const [loading, setLoading] = useState<boolean>(false);
const [approvalPolicy, setApprovalPolicy] = useState<ApprovalPolicy>(
initialApprovalPolicy,
Expand Down Expand Up @@ -242,7 +252,6 @@ export default function TerminalChat({
// Tear down any existing loop before creating a new one.
agentRef.current?.terminate();

const sessionId = crypto.randomUUID();
agentRef.current = new AgentLoop({
model,
provider,
Expand All @@ -257,6 +266,17 @@ export default function TerminalChat({
setItems((prev) => {
const updated = uniqueById([...prev, item as ResponseItem]);
saveRollout(sessionId, updated);
if (memoryEnabled) {
const sessionInfo = {
id: sessionId,
user: "",
version: CLI_VERSION,
model,
timestamp: new Date().toISOString(),
instructions: config.instructions,
};
saveSessionMemory(sessionInfo, updated);
}
return updated;
});
},
Expand Down Expand Up @@ -526,6 +546,7 @@ export default function TerminalChat({
openApprovalOverlay={() => setOverlayMode("approval")}
openHelpOverlay={() => setOverlayMode("help")}
openSessionsOverlay={() => setOverlayMode("sessions")}
openMemoryOverlay={() => setOverlayMode("memory")}
openDiffOverlay={() => {
const { isGitRepo, diff } = getGitDiff();
let text: string;
Expand Down Expand Up @@ -754,6 +775,101 @@ export default function TerminalChat({
<HelpOverlay onExit={() => setOverlayMode("none")} />
)}

{overlayMode === "memory" && (
<MemoryOverlay
memoryEnabled={memoryEnabled}
onSelect={async (option) => {
if (option === 'on') {
// Enable memory
const updatedConfig = {
...config,
memory: {
...config.memory,
enabled: true,
},
};
saveConfig(updatedConfig);
setMemoryEnabled(true);
const sessionInfo = {
id: sessionId,
user: "",
version: CLI_VERSION,
model,
timestamp: new Date().toISOString(),
instructions: config.instructions,
};
saveSessionMemory(sessionInfo, items);

setItems((prev) => [
...prev,
{
id: `memory-enabled-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: "✓ Memory enabled - session data will be persisted",
},
],
},
]);
} else if (option === 'off') {
// Disable memory
const updatedConfig = {
...config,
memory: {
...config.memory,
enabled: false,
},
};
saveConfig(updatedConfig);
setMemoryEnabled(false);

setItems((prev) => [
...prev,
{
id: `memory-disabled-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: "✓ Memory disabled - session data will not be persisted",
},
],
},
]);
} else if (option === 'clear') {
// Clear memory
const { clearSessionMemory } = await import("../../utils/storage/session-memory.js");
clearSessionMemory();

const sessionDir = path.join(process.cwd(), ".codex");
const sessionFile = path.join(sessionDir, "session.json");
const sessionFilePath = sessionFile.replace(os.homedir(), "~");

setItems((prev) => [
...prev,
{
id: `memory-cleared-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: `✓ Memory cleared - session data removed from ${sessionFilePath}`,
},
],
},
]);
}
setOverlayMode("none");
}}
onExit={() => setOverlayMode("none")}
/>
)}

{overlayMode === "diff" && (
<DiffOverlay
diffText={diffText}
Expand Down
73 changes: 73 additions & 0 deletions codex-cli/src/components/memory-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import TypeaheadOverlay from "./typeahead-overlay.js";
import { Box, Text } from "ink";
import os from "os";
import path from "path";
import React from "react";

/**
* Props for <MemoryOverlay>.
*/
type Props = {
onSelect: (option: string) => void;
onExit: () => void;
memoryEnabled: boolean;
};

export default function MemoryOverlay({
onSelect,
onExit,
memoryEnabled,
}: Props): JSX.Element {

// Get the session file path
const sessionDir = path.join(process.cwd(), ".codex");
const sessionFile = path.join(sessionDir, "session.json");
const sessionFilePath = sessionFile.replace(os.homedir(), "~");

const items = [
{
label: `Enable session persistence`,
value: 'on',
},
{
label: `Disable session persistence`,
value: 'off',
},
{
label: `Clear session data from ${sessionFilePath}`,
value: 'clear',
},
];

// Filter items based on current state
const filteredItems = items.filter((item) => {
if (item.value === 'on' && memoryEnabled) {
return false;
}
if (item.value === 'off' && !memoryEnabled) {
return false;
}
return true;
});

return (
<TypeaheadOverlay
title="Memory Management"
description={
<Box flexDirection="column">
<Text>
Current status: <Text color={memoryEnabled ? "greenBright" : "red"}>
{memoryEnabled ? "enabled" : "disabled"}
</Text>
</Text>
<Text dimColor>
Session data is stored in: {sessionFilePath}
</Text>
</Box>
}
initialItems={filteredItems}
onSelect={onSelect}
onExit={onExit}
/>
);
}
4 changes: 4 additions & 0 deletions codex-cli/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,10 @@ export const saveConfig = (
flexMode: config.flexMode,
reasoningEffort: config.reasoningEffort,
};
// Persist memory settings when explicitly configured
if (config.memory !== undefined) {
configToSave.memory = config.memory;
}

// Add history settings if they exist
if (config.history) {
Expand Down
4 changes: 4 additions & 0 deletions codex-cli/src/utils/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ export const SLASH_COMMANDS: Array<SlashCommand> = [
description:
"Show git diff of the working directory (or applied patches if not in git)",
},
{
command: "/memory",
description: "Manage project-local session memory (status, clear)",
},
];
Loading