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
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,26 @@ In `agentic/agents/`:
1. Create agent file
2. Define prompt in `prompts/`
3. Register in `registry.rs`

## Frontend Debugging

A local log receiver server is available at `scripts/debug-log-server.mjs`.

**Start the server:**
```bash
node scripts/debug-log-server.mjs
# Listens on http://127.0.0.1:7469, writes logs to debug-agent.log
```

**Instrument code (one-liner fetch):**
```typescript
fetch('http://127.0.0.1:7469/log',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'file.ts:LINE',message:'desc',data:{k:v},timestamp:Date.now()})}).catch(()=>{});
```

**Clear logs between runs:**
```bash
# Via HTTP
curl -X POST http://127.0.0.1:7469/clear
```

Logs are written to `debug-agent.log` in project root as NDJSON. The agent reads this file directly — no copy-paste needed.
86 changes: 86 additions & 0 deletions scripts/debug-log-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Debug Log Receiver Server
* Receives POST requests from fetch-based instrumentation and writes to a log file.
*
* Usage:
* node scripts/debug-log-server.mjs [port] [logfile]
*
* Defaults:
* port = 7469
* logfile = debug-agent.log (in project root)
*
* Frontend fetch template (copy into your code):
* fetch('http://127.0.0.1:7469/log', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({location:'file.ts:LINE', message:'desc', data:{k:v}, timestamp:Date.now()})}).catch(()=>{});
*/

import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');

const PORT = parseInt(process.argv[2] ?? '7469', 10);
const LOG_FILE = path.resolve(ROOT, process.argv[3] ?? 'debug-agent.log');

// Clear log file on start
fs.writeFileSync(LOG_FILE, '', 'utf8');
console.log(`[debug-log-server] started on http://127.0.0.1:${PORT}`);
console.log(`[debug-log-server] writing logs to ${LOG_FILE}`);
console.log(`[debug-log-server] fetch template:`);
console.log(` fetch('http://127.0.0.1:${PORT}/log', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({location:'file.ts:LINE', message:'desc', data:{}, timestamp:Date.now()})}).catch(()=>{});\n`);

const server = http.createServer((req, res) => {
// CORS headers so browser can POST from any origin
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}

if (req.method === 'POST' && req.url === '/log') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
try {
const payload = JSON.parse(body);
const entry = JSON.stringify({ ...payload, _receivedAt: new Date().toISOString() });
fs.appendFileSync(LOG_FILE, entry + '\n', 'utf8');

// Pretty print to console for quick monitoring
const loc = payload.location ?? '?';
const msg = payload.message ?? '';
const data = payload.data ? JSON.stringify(payload.data) : '';
console.log(`[LOG] ${loc} | ${msg} | ${data}`);

res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'invalid json' }));
}
});
return;
}

// /clear - clear log file
if (req.method === 'POST' && req.url === '/clear') {
fs.writeFileSync(LOG_FILE, '', 'utf8');
console.log('[debug-log-server] log file cleared');
res.writeHead(200);
res.end(JSON.stringify({ ok: true }));
return;
}

res.writeHead(404);
res.end();
});

server.listen(PORT, '127.0.0.1', () => {
console.log('[debug-log-server] ready\n');
});
Original file line number Diff line number Diff line change
Expand Up @@ -258,73 +258,8 @@ export async function switchChatSession(
): Promise<void> {
try {
const session = context.flowChatStore.getState().sessions.get(sessionId);

if (session?.isHistorical) {
try {
const workspacePath = requireSessionWorkspacePath(session.workspacePath, sessionId);

await context.flowChatStore.loadSessionHistory(
sessionId,
workspacePath,
undefined,
session.remoteConnectionId,
session.remoteSshHost
);

try {
await agentAPI.restoreSession(
sessionId,
workspacePath,
session.remoteConnectionId,
session.remoteSshHost
);

context.flowChatStore.setState(prev => {
const newSessions = new Map(prev.sessions);
const sess = newSessions.get(sessionId);
if (sess) {
newSessions.set(sessionId, { ...sess, isHistorical: false });
}
return { ...prev, sessions: newSessions };
});
} catch (restoreError: any) {
log.warn('Historical session restore failed, creating new session', { sessionId, error: restoreError });
const currentSession = context.flowChatStore.getState().sessions.get(sessionId);
if (currentSession) {
await agentAPI.createSession({
sessionId: sessionId,
sessionName: currentSession.title || `Session ${sessionId.slice(0, 8)}`,
agentType: currentSession.mode || 'agentic',
workspacePath,
remoteConnectionId: currentSession.remoteConnectionId,
remoteSshHost: currentSession.remoteSshHost,
config: {
modelName: currentSession.config.modelName || 'auto',
enableTools: true,
safeMode: true,
remoteConnectionId: currentSession.remoteConnectionId,
remoteSshHost: currentSession.remoteSshHost,
}
});

context.flowChatStore.setState(prev => {
const newSessions = new Map(prev.sessions);
const sess = newSessions.get(sessionId);
if (sess) {
newSessions.set(sessionId, { ...sess, isHistorical: false });
}
return { ...prev, sessions: newSessions };
});
}
}
} catch (error) {
log.error('Failed to load session history', { sessionId, error });
notificationService.warning('Failed to load session history, showing empty session', {
duration: 3000
});
}
}


// Switch UI immediately so the user sees the new session without waiting for history load.
context.flowChatStore.switchSession(sessionId);

touchSessionActivity(
Expand All @@ -335,6 +270,29 @@ export async function switchChatSession(
).catch(error => {
log.debug('Failed to touch session activity', { sessionId, error });
});

if (session?.isHistorical) {
// Load history in the background — do not block the UI.
(async () => {
try {
const workspacePath = requireSessionWorkspacePath(session.workspacePath, sessionId);

// loadSessionHistory internally calls restoreSession + loadSessionTurns.
await context.flowChatStore.loadSessionHistory(
sessionId,
workspacePath,
undefined,
session.remoteConnectionId,
session.remoteSshHost
);
} catch (error) {
log.error('Failed to load session history', { sessionId, error });
notificationService.warning('Failed to load session history, showing empty session', {
duration: 3000
});
}
})();
}
} catch (error) {
log.error('Failed to switch chat session', { sessionId, error });
notificationService.error('Failed to switch session', {
Expand Down
Loading