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

View File

@@ -0,0 +1,138 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('./agent-runner.js', () => ({
runAgentProcess: vi.fn(),
writeGroupsSnapshot: vi.fn(),
writeTasksSnapshot: vi.fn(),
}));
vi.mock('./available-groups.js', () => ({
listAvailableGroups: vi.fn(() => []),
}));
vi.mock('./config.js', () => ({
DATA_DIR: '/tmp/ejclaw-test-data',
}));
vi.mock('./db.js', () => ({
getAllTasks: vi.fn(() => []),
}));
vi.mock('./logger.js', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
vi.mock('./provider-fallback.js', () => ({
detectFallbackTrigger: vi.fn(() => ({
shouldFallback: false,
reason: '',
})),
getActiveProvider: vi.fn(() => 'claude'),
getFallbackEnvOverrides: vi.fn(() => ({})),
getFallbackProviderName: vi.fn(() => 'fallback'),
hasGroupProviderOverride: vi.fn(() => false),
isFallbackEnabled: vi.fn(() => false),
markPrimaryCooldown: vi.fn(),
}));
vi.mock('./session-recovery.js', () => ({
shouldResetSessionOnAgentFailure: vi.fn(() => false),
}));
vi.mock('./memento-client.js', () => ({
buildRoomMemoryBriefing: vi.fn(),
}));
import { runAgentProcess } from './agent-runner.js';
import { buildRoomMemoryBriefing } from './memento-client.js';
import { runAgentForGroup } from './message-agent-executor.js';
import type { RegisteredGroup } from './types.js';
const testGroup: RegisteredGroup = {
name: 'Test Group',
folder: 'test-group',
trigger: '@Andy',
added_at: new Date().toISOString(),
};
const deps = {
assistantName: 'Andy',
queue: {
registerProcess: vi.fn(),
},
getRegisteredGroups: vi.fn(() => ({})),
getSessions: vi.fn(() => ({})),
persistSession: vi.fn(),
clearSession: vi.fn(),
};
describe('runAgentForGroup', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(runAgentProcess).mockResolvedValue({
status: 'success',
result: 'ok',
newSessionId: 'session-123',
});
vi.mocked(buildRoomMemoryBriefing).mockResolvedValue(
'## Shared Room Memory\n- remembered context',
);
});
it('injects a room memory briefing when starting a fresh session', async () => {
deps.getSessions.mockReturnValue({});
const result = await runAgentForGroup(deps, {
group: testGroup,
prompt: 'hello',
chatJid: 'group@test',
runId: 'run-1',
});
expect(result).toBe('success');
expect(buildRoomMemoryBriefing).toHaveBeenCalledWith({
groupFolder: 'test-group',
groupName: 'Test Group',
});
expect(runAgentProcess).toHaveBeenCalledWith(
testGroup,
expect.objectContaining({
prompt: 'hello',
sessionId: undefined,
memoryBriefing: '## Shared Room Memory\n- remembered context',
}),
expect.any(Function),
undefined,
undefined,
);
});
it('skips the room memory briefing for existing sessions', async () => {
deps.getSessions.mockReturnValue({ 'test-group': 'session-existing' });
const result = await runAgentForGroup(deps, {
group: testGroup,
prompt: 'hello again',
chatJid: 'group@test',
runId: 'run-2',
});
expect(result).toBe('success');
expect(buildRoomMemoryBriefing).not.toHaveBeenCalled();
expect(runAgentProcess).toHaveBeenCalledWith(
testGroup,
expect.objectContaining({
prompt: 'hello again',
sessionId: 'session-existing',
memoryBriefing: undefined,
}),
expect.any(Function),
undefined,
undefined,
);
});
});