Skip to content

Commit 2d9ca23

Browse files
ozgesolidkeyclaude
andcommitted
Add agent memory — persistent per-file context for AI sessions
Agents can now save and read a memory note tied to the current log file so analysis can resume where it left off across sessions. Storage: <logdir>/.logan/<filename>.agent-memory.json sidecar file. Auto-inject on connect: when a named agent connects via /api/events the SSE connected event includes the current memory payload so the agent gets prior context without explicitly calling logan_memory_read. MCP tools: logan_memory_read — read the saved memory for the current file logan_memory_write — write/update the memory note API endpoints: GET /api/agent-memory — read POST /api/agent-memory — write (content + optional agentName) POST /api/agent-memory-clear — delete IPC: agent-memory-get/save/clear + agent-memory-changed push event. UI: a compact memory bar in the Chat panel shows the agent name, time ago, and a one-line preview of the last saved note. Disappears when no memory exists for the current file; ✕ button to clear. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 413c30b commit 2d9ca23

8 files changed

Lines changed: 206 additions & 1 deletion

File tree

src/main/api-server.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ export function getAgentName(): string | null {
6868
return getConnectedAgentName();
6969
}
7070

71+
function notifyMemoryChanged(ctx: ApiContext): void {
72+
const win = ctx.getMainWindow();
73+
if (win && !win.isDestroyed()) {
74+
win.webContents.send('agent-memory-changed', ctx.getAgentMemory());
75+
}
76+
}
77+
7178
function notifyAgentConnectionChanged(ctx: ApiContext): void {
7279
const connected = isAgentConnected();
7380
const name = getConnectedAgentName();
@@ -132,6 +139,9 @@ export interface ApiContext {
132139
clearHighlights(): any;
133140
loadNotes(): Promise<any>;
134141
saveNotes(content: string): Promise<any>;
142+
getAgentMemory(): any;
143+
saveAgentMemory(content: string, agentName?: string): any;
144+
clearAgentMemory(): any;
135145
detectTimeGaps(options: any): Promise<any>;
136146
navigateToLine(lineNumber: number): void;
137147
getBaselineStore(): BaselineStore;
@@ -257,6 +267,12 @@ export function startApiServer(ctx: ApiContext): void {
257267
return;
258268
}
259269

270+
if (url === '/api/agent-memory') {
271+
const mem = ctx.getAgentMemory();
272+
sendJson(res, { success: true, memory: mem || null });
273+
return;
274+
}
275+
260276
if (url === '/api/agent-status') {
261277
sendJson(res, {
262278
success: true,
@@ -283,7 +299,9 @@ export function startApiServer(ctx: ApiContext): void {
283299
sendJson(res, { success: false, error: 'Another agent is already connected', connectedAgent: activeAgent.name }, 409);
284300
return;
285301
}
286-
res.write(`event: connected\ndata: ${JSON.stringify({ name: agentName })}\n\n`);
302+
// Include any existing agent memory so the agent can resume context
303+
const memory = ctx.getAgentMemory();
304+
res.write(`event: connected\ndata: ${JSON.stringify({ name: agentName, memory: memory || null })}\n\n`);
287305
activeAgent = { res, name: agentName };
288306
notifyAgentConnectionChanged(ctx);
289307
req.on('close', () => {
@@ -497,6 +515,21 @@ export function startApiServer(ctx: ApiContext): void {
497515
return;
498516
}
499517

518+
if (url === '/api/agent-memory') {
519+
if (body.content === undefined) return sendError(res, 'content required');
520+
const ok = ctx.saveAgentMemory(body.content, body.agentName || activeAgent?.name);
521+
notifyMemoryChanged(ctx);
522+
sendJson(res, { success: ok });
523+
return;
524+
}
525+
526+
if (url === '/api/agent-memory-clear') {
527+
ctx.clearAgentMemory();
528+
notifyMemoryChanged(ctx);
529+
sendJson(res, { success: true });
530+
return;
531+
}
532+
500533
if (url === '/api/time-gaps') {
501534
const result = await ctx.detectTimeGaps({
502535
thresholdSeconds: body.thresholdSeconds ?? 30,

src/main/index.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,10 @@ app.whenReady().then(() => {
892892
return { success: true, content: '' };
893893
}
894894
},
895+
getAgentMemory: () => getAgentMemory(currentFilePath),
896+
saveAgentMemory: (content: string, agentName?: string) =>
897+
saveAgentMemory(currentFilePath, content, agentName),
898+
clearAgentMemory: () => clearAgentMemory(currentFilePath),
895899
saveNotes: async (content: string) => {
896900
if (!currentFilePath) return { success: false, error: 'No file open' };
897901
if (!ensureLocalLoganDir(currentFilePath)) {
@@ -3725,6 +3729,59 @@ ipcMain.handle('save-selected-lines', async (_, startLine: number, endLine: numb
37253729
}
37263730
});
37273731

3732+
// === Agent Memory ===
3733+
// Per-file persistent memory for AI agents. Stored alongside the log file so
3734+
// agents can resume analysis across sessions.
3735+
3736+
interface AgentMemoryData {
3737+
content: string;
3738+
agentName: string;
3739+
updatedAt: number;
3740+
}
3741+
3742+
function agentMemoryPath(filePath: string | null): string | null {
3743+
if (!filePath) return null;
3744+
return path.join(getLocalLoganDir(filePath), path.basename(filePath) + '.agent-memory.json');
3745+
}
3746+
3747+
function getAgentMemory(filePath: string | null): AgentMemoryData | null {
3748+
const p = agentMemoryPath(filePath);
3749+
if (!p) return null;
3750+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
3751+
}
3752+
3753+
function saveAgentMemory(filePath: string | null, content: string, agentName?: string): boolean {
3754+
if (!filePath) return false;
3755+
if (!ensureLocalLoganDir(filePath)) return false;
3756+
const p = agentMemoryPath(filePath)!;
3757+
const data: AgentMemoryData = {
3758+
content,
3759+
agentName: agentName || 'Agent',
3760+
updatedAt: Date.now(),
3761+
};
3762+
try { fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf-8'); return true; } catch { return false; }
3763+
}
3764+
3765+
function clearAgentMemory(filePath: string | null): boolean {
3766+
const p = agentMemoryPath(filePath);
3767+
if (!p) return false;
3768+
try { if (fs.existsSync(p)) fs.unlinkSync(p); return true; } catch { return false; }
3769+
}
3770+
3771+
ipcMain.handle('agent-memory-get', () => {
3772+
const mem = getAgentMemory(currentFilePath);
3773+
return mem || null;
3774+
});
3775+
3776+
ipcMain.handle('agent-memory-save', (_e, content: string, agentName?: string) => {
3777+
const ok = saveAgentMemory(currentFilePath, content, agentName);
3778+
return { success: ok };
3779+
});
3780+
3781+
ipcMain.handle('agent-memory-clear', () => {
3782+
return { success: clearAgentMemory(currentFilePath) };
3783+
});
3784+
37283785
// === Save to Notes ===
37293786

37303787
// Helper: Read notes file header to get source log path

src/mcp-server/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,33 @@ server.tool(
791791
}
792792
);
793793

794+
// === Tool: logan_memory_read ===
795+
server.tool(
796+
'logan_memory_read',
797+
'Read the agent memory note for the current log file. Memory persists across sessions so you can resume analysis where you left off.',
798+
{},
799+
async () => {
800+
const result = await apiCall('GET', '/api/agent-memory');
801+
if (!result.memory) return { content: [{ type: 'text', text: 'No memory saved for this file yet.' }] };
802+
const { content, agentName, updatedAt } = result.memory;
803+
const ts = new Date(updatedAt).toLocaleString();
804+
return { content: [{ type: 'text', text: `Memory saved by ${agentName} at ${ts}:\n\n${content}` }] };
805+
}
806+
);
807+
808+
// === Tool: logan_memory_write ===
809+
server.tool(
810+
'logan_memory_write',
811+
'Save a memory note for the current log file. Use this to record findings, progress, and next steps so you can resume analysis in a future session.',
812+
{
813+
content: z.string().describe('The memory content to save — findings, current status, next steps, etc.'),
814+
},
815+
async ({ content }) => {
816+
await apiCall('POST', '/api/agent-memory', { content });
817+
return { content: [{ type: 'text', text: 'Memory saved.' }] };
818+
}
819+
);
820+
794821
// === Tool: logan_remove_bookmark ===
795822
server.tool(
796823
'logan_remove_bookmark',

src/preload/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,18 @@ const api = {
518518
return () => ipcRenderer.removeListener('annotations-changed', handler);
519519
},
520520

521+
getAgentMemory: (): Promise<{ content: string; agentName: string; updatedAt: number } | null> =>
522+
ipcRenderer.invoke('agent-memory-get'),
523+
saveAgentMemory: (content: string, agentName?: string): Promise<{ success: boolean }> =>
524+
ipcRenderer.invoke('agent-memory-save', content, agentName),
525+
clearAgentMemory: (): Promise<{ success: boolean }> =>
526+
ipcRenderer.invoke('agent-memory-clear'),
527+
onAgentMemoryChanged: (callback: (memory: any) => void): (() => void) => {
528+
const handler = (_: any, mem: any) => callback(mem);
529+
ipcRenderer.on('agent-memory-changed', handler);
530+
return () => ipcRenderer.removeListener('agent-memory-changed', handler);
531+
},
532+
521533
// Device discovery
522534
serialListPorts: (): Promise<{ success: boolean; ports?: any[]; error?: string }> =>
523535
ipcRenderer.invoke(IPC.SERIAL_LIST_PORTS),

src/renderer/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,11 @@ <h2>LOGAN</h2>
630630
<button id="chat-launch-agent" class="chat-launch-btn">Launch Agent</button>
631631
<!-- Gear button removed — Launch Agent now always opens the wizard -->
632632
</div>
633+
<div id="chat-memory-bar" class="chat-memory-bar hidden">
634+
<span class="chat-memory-icon">🧠</span>
635+
<span id="chat-memory-label" class="chat-memory-label">Memory saved</span>
636+
<button id="btn-clear-agent-memory" class="chat-memory-clear" title="Clear agent memory"></button>
637+
</div>
633638
<div id="chat-messages" class="chat-messages"></div>
634639
<div class="chat-input-bar">
635640
<input id="chat-input" type="text" placeholder="Message the AI agent..." />

src/renderer/renderer.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5159,6 +5159,8 @@ function openBottomTab(tabId: string): void {
51595159
if (badge) badge.textContent = '';
51605160
// Refresh agent connection status
51615161
window.api.getAgentStatus().then((s: any) => updateAgentConnectionStatus(s.connected, s.count, s.name));
5162+
// Refresh memory bar
5163+
window.api.getAgentMemory().then((mem: any) => updateAgentMemoryBar(mem));
51625164
}
51635165
if (tabId === 'live') {
51645166
refreshLiveDevices();
@@ -5684,6 +5686,29 @@ function updateAgentConnectionStatus(connected: boolean, count: number, name?: s
56845686
}
56855687
}
56865688

5689+
function updateAgentMemoryBar(memory: { content: string; agentName: string; updatedAt: number } | null): void {
5690+
const bar = document.getElementById('chat-memory-bar');
5691+
const label = document.getElementById('chat-memory-label');
5692+
if (!bar || !label) return;
5693+
if (!memory) {
5694+
bar.classList.add('hidden');
5695+
return;
5696+
}
5697+
const ago = formatTimeAgo(memory.updatedAt);
5698+
const preview = memory.content.split('\n')[0].slice(0, 60);
5699+
label.textContent = `${memory.agentName} · ${ago} — ${preview}${memory.content.length > 60 ? '…' : ''}`;
5700+
label.title = memory.content;
5701+
bar.classList.remove('hidden');
5702+
}
5703+
5704+
function formatTimeAgo(ts: number): string {
5705+
const diff = Date.now() - ts;
5706+
if (diff < 60000) return 'just now';
5707+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
5708+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
5709+
return `${Math.floor(diff / 86400000)}d ago`;
5710+
}
5711+
56875712
let agentRunning = false;
56885713

56895714
function updateLaunchButton(): void {
@@ -13223,6 +13248,16 @@ function init(): void {
1322313248
updateLaunchButton();
1322413249
}
1322513250
});
13251+
// Agent memory push listener
13252+
window.api.onAgentMemoryChanged((memory: any) => {
13253+
updateAgentMemoryBar(memory);
13254+
});
13255+
13256+
// Clear agent memory button
13257+
document.getElementById('btn-clear-agent-memory')?.addEventListener('click', async () => {
13258+
await window.api.clearAgentMemory();
13259+
});
13260+
1322613261
// Agent annotations push listener
1322713262
window.api.onAnnotationsChanged((anns: any[]) => {
1322813263
state.annotations = anns;

src/renderer/styles.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4153,6 +4153,37 @@ kbd {
41534153
font-style: italic;
41544154
}
41554155

4156+
.chat-memory-bar {
4157+
display: flex;
4158+
align-items: center;
4159+
gap: 6px;
4160+
padding: 4px 10px;
4161+
background: rgba(100, 80, 180, 0.12);
4162+
border-bottom: 1px solid rgba(100, 80, 180, 0.25);
4163+
font-size: 11px;
4164+
color: var(--text-secondary);
4165+
min-height: 0;
4166+
}
4167+
.chat-memory-bar.hidden { display: none; }
4168+
.chat-memory-icon { font-size: 13px; flex-shrink: 0; }
4169+
.chat-memory-label {
4170+
flex: 1;
4171+
overflow: hidden;
4172+
text-overflow: ellipsis;
4173+
white-space: nowrap;
4174+
cursor: default;
4175+
}
4176+
.chat-memory-clear {
4177+
background: none;
4178+
border: none;
4179+
color: var(--text-muted);
4180+
cursor: pointer;
4181+
font-size: 11px;
4182+
padding: 0 2px;
4183+
flex-shrink: 0;
4184+
}
4185+
.chat-memory-clear:hover { color: var(--error-color); }
4186+
41564187
.chat-launch-btn {
41574188
background: var(--accent-color, #007acc);
41584189
color: #fff;

src/renderer/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,11 @@ interface Api {
470470
clearAnnotations: () => Promise<{ success: boolean }>;
471471
onAnnotationsChanged: (callback: (annotations: any[]) => void) => () => void;
472472

473+
getAgentMemory: () => Promise<{ content: string; agentName: string; updatedAt: number } | null>;
474+
saveAgentMemory: (content: string, agentName?: string) => Promise<{ success: boolean }>;
475+
clearAgentMemory: () => Promise<{ success: boolean }>;
476+
onAgentMemoryChanged: (callback: (memory: any) => void) => () => void;
477+
473478
// Device discovery
474479
serialListPorts: () => Promise<{ success: boolean; ports?: Array<{ path: string; manufacturer?: string; vendorId?: string; productId?: string }>; error?: string }>;
475480
logcatListDevices: () => Promise<{ success: boolean; devices?: Array<{ id: string; state: string; model?: string }>; error?: string }>;

0 commit comments

Comments
 (0)