feat: auto memory pipeline — host-driven Memento recall/reflect

Add automatic memory integration so EJClaw host directly calls Memento
MCP for recall at session start and reflect on compact, removing
reliance on agent voluntary tool use.

Stage 1: New sessions get room memory briefing injected into system
prompt (CLAUDE.md / AGENTS.md) via host-side MCP client.

Stage 2: PreCompact hook automatically calls reflect + remember to
persist session summaries as room-memory fragments.

Also restores missing source files (token-rotation, codex-token-rotation,
claude-usage exports) to fix full build from source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-24 03:48:36 +09:00
parent 8c98ad9faf
commit 3467e245d5
14 changed files with 2406 additions and 178 deletions

166
src/codex-token-rotation.ts Normal file
View File

@@ -0,0 +1,166 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
export interface CodexAccountState {
index: number;
planType: string;
isActive: boolean;
isRateLimited: boolean;
cachedUsagePct?: number;
resetAt?: string;
cachedUsageD7Pct?: number;
resetD7At?: string;
}
const HOST_CODEX_DIR = path.join(os.homedir(), '.codex');
const CODEX_ACCOUNTS_DIR = path.join(os.homedir(), '.codex-accounts');
let activeIndexCache: number | null = null;
const rateLimitedAccounts = new Set<number>();
const usageCache = new Map<
number,
{
cachedUsagePct?: number;
resetAt?: string;
cachedUsageD7Pct?: number;
resetD7At?: string;
}
>();
function listCodexAccountDirs(): string[] {
if (!fs.existsSync(CODEX_ACCOUNTS_DIR)) return [];
return fs
.readdirSync(CODEX_ACCOUNTS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name))
.sort((a, b) => Number(a.name) - Number(b.name))
.map((entry) => path.join(CODEX_ACCOUNTS_DIR, entry.name));
}
function readTextIfExists(filePath: string): string | null {
try {
if (!fs.existsSync(filePath)) return null;
return fs.readFileSync(filePath, 'utf8');
} catch {
return null;
}
}
function inferPlanType(accountDir: string): string {
try {
const authPath = path.join(accountDir, 'auth.json');
if (!fs.existsSync(authPath)) return 'API';
const raw = JSON.parse(fs.readFileSync(authPath, 'utf8')) as {
auth_mode?: string;
};
if (!raw.auth_mode) return 'API';
return raw.auth_mode
.split(/[_-]/)
.filter(Boolean)
.map((part) => part[0].toUpperCase() + part.slice(1))
.join(' ');
} catch {
return 'API';
}
}
function resolveActiveIndex(accountDirs: string[]): number {
if (accountDirs.length <= 1) return 0;
if (activeIndexCache !== null && activeIndexCache < accountDirs.length) {
return activeIndexCache;
}
const hostAuth = readTextIfExists(path.join(HOST_CODEX_DIR, 'auth.json'));
if (hostAuth) {
const matchIndex = accountDirs.findIndex(
(accountDir) => readTextIfExists(path.join(accountDir, 'auth.json')) === hostAuth,
);
if (matchIndex >= 0) {
activeIndexCache = matchIndex;
return matchIndex;
}
}
activeIndexCache = 0;
return 0;
}
function copyIfExists(src: string, dst: string): void {
if (!fs.existsSync(src)) return;
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.copyFileSync(src, dst);
}
function syncHostCodexAccount(accountDir: string): void {
copyIfExists(path.join(accountDir, 'auth.json'), path.join(HOST_CODEX_DIR, 'auth.json'));
}
export function getAllCodexAccounts(): CodexAccountState[] {
const accountDirs = listCodexAccountDirs();
if (accountDirs.length === 0) {
return [
{
index: 0,
planType: 'API',
isActive: true,
isRateLimited: false,
},
];
}
const activeIndex = resolveActiveIndex(accountDirs);
return accountDirs.map((accountDir, index) => ({
index,
planType: inferPlanType(accountDir),
isActive: index === activeIndex,
isRateLimited: rateLimitedAccounts.has(index),
...usageCache.get(index),
}));
}
export function getCodexAccountCount(): number {
return getAllCodexAccounts().length;
}
export function rotateCodexToken(_reason?: string): boolean {
const accountDirs = listCodexAccountDirs();
if (accountDirs.length <= 1) return false;
const activeIndex = resolveActiveIndex(accountDirs);
rateLimitedAccounts.add(activeIndex);
const candidates = accountDirs.map((_, index) => index);
const nextIndex =
candidates.find(
(index) => index !== activeIndex && !rateLimitedAccounts.has(index),
) ??
candidates.find((index) => index !== activeIndex);
if (nextIndex === undefined) return false;
syncHostCodexAccount(accountDirs[nextIndex]);
activeIndexCache = nextIndex;
return true;
}
export function markCodexTokenHealthy(): void {
const accountDirs = listCodexAccountDirs();
const activeIndex = resolveActiveIndex(accountDirs);
rateLimitedAccounts.delete(activeIndex);
}
export function updateCodexAccountUsage(
pct: number,
resetAt: string | undefined,
index: number,
d7Pct?: number,
resetD7At?: string,
): void {
usageCache.set(index, {
cachedUsagePct: pct,
resetAt,
cachedUsageD7Pct: d7Pct,
resetD7At,
});
}