mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] shell: crashes write a report the bug bundle attaches, agents finishing off-screen tap the OS, text size is a slider
This commit is contained in:
@@ -91,6 +91,26 @@ def p_count_dir(path: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_recent_crash_reports(limit: int = 3) -> str:
|
||||
"""The newest Electron crash reports (ENG-102), metadata only: the backend-log tail inside each
|
||||
report is dropped here because this bundle already carries its own scrubbed tail."""
|
||||
try:
|
||||
crash_dir = os.path.join(os.path.dirname(DATA_ROOT), "crash-reports")
|
||||
files = sorted((f for f in os.listdir(crash_dir) if f.endswith(".json")), reverse=True)[:limit]
|
||||
if not files:
|
||||
return "(none)"
|
||||
out: List[str] = []
|
||||
for name in files:
|
||||
with open(os.path.join(crash_dir, name), "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
data.pop("backendLogTail", None)
|
||||
out.append(json.dumps(data, indent=2, default=str))
|
||||
return "\n".join(out)
|
||||
except Exception:
|
||||
return "(none)"
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_build_report(req: BundleRequest) -> str:
|
||||
from backend.apps.settings.store import load_settings
|
||||
@@ -135,6 +155,11 @@ def p_build_report(req: BundleRequest) -> str:
|
||||
p_log_tail(),
|
||||
"```",
|
||||
"",
|
||||
"## Recent crashes",
|
||||
"```json",
|
||||
p_recent_crash_reports(),
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// ENG-102: a crash without a report blinds every other crash bug. Each fatal signal writes one
|
||||
// JSON report (metadata + the backend log tail) into userData/crash-reports and, when possible,
|
||||
// tells the user where it landed. Renderer/GPU deaths and main-process throws all route here.
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const MAX_REPORTS = 30;
|
||||
const LOG_TAIL_BYTES = 64 * 1024;
|
||||
|
||||
let p_app = null;
|
||||
let p_notify = null;
|
||||
|
||||
function init(app, notifyFn) {
|
||||
p_app = app;
|
||||
p_notify = notifyFn || null;
|
||||
}
|
||||
|
||||
function reportsDir() {
|
||||
const dir = path.join(p_app.getPath('userData'), 'crash-reports');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function backendLogTail() {
|
||||
try {
|
||||
const logPath = path.join(p_app.getPath('userData'), 'data', 'backend.log');
|
||||
const size = fs.statSync(logPath).size;
|
||||
const fd = fs.openSync(logPath, 'r');
|
||||
const start = Math.max(0, size - LOG_TAIL_BYTES);
|
||||
const buf = Buffer.alloc(size - start);
|
||||
fs.readSync(fd, buf, 0, buf.length, start);
|
||||
fs.closeSync(fd);
|
||||
return buf.toString('utf8');
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function prune(dir) {
|
||||
try {
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
||||
while (files.length > MAX_REPORTS) fs.unlinkSync(path.join(dir, files.shift()));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function writeCrashReport(kind, details) {
|
||||
try {
|
||||
const dir = reportsDir();
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const file = path.join(dir, `crash-${stamp}-${kind}.json`);
|
||||
const report = {
|
||||
kind,
|
||||
at: new Date().toISOString(),
|
||||
appVersion: p_app.getVersion(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
electron: process.versions.electron,
|
||||
details,
|
||||
backendLogTail: backendLogTail(),
|
||||
};
|
||||
fs.writeFileSync(file, JSON.stringify(report, null, 2));
|
||||
prune(dir);
|
||||
if (p_notify) {
|
||||
p_notify({
|
||||
title: 'OpenSwarm hit a problem',
|
||||
body: 'A crash report was saved. Help > Report a bug attaches it automatically.',
|
||||
});
|
||||
}
|
||||
return file;
|
||||
} catch (err) {
|
||||
console.error('[crash-reports] failed to write report:', err && err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Reports written since the previous launch; the renderer surfaces "last session crashed".
|
||||
function unseenReports() {
|
||||
try {
|
||||
const dir = reportsDir();
|
||||
const marker = path.join(dir, '.last-seen');
|
||||
let last = 0;
|
||||
try { last = fs.statSync(marker).mtimeMs; } catch (_) {}
|
||||
const fresh = fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith('.json'))
|
||||
.map((f) => path.join(dir, f))
|
||||
.filter((p) => { try { return fs.statSync(p).mtimeMs > last; } catch (_) { return false; } });
|
||||
fs.writeFileSync(marker, String(Date.now()));
|
||||
return fresh;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { init, writeCrashReport, unseenReports };
|
||||
@@ -88,8 +88,11 @@ try {
|
||||
}
|
||||
|
||||
// Capture every main-process throw we can. Without these, a throw inside an IPC handler or BrowserWindow event listener can die silently and look indistinguishable from a renderer crash in the trace.
|
||||
const crashReports = require('./crashReports');
|
||||
crashReports.init(app, null);
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[diag][main:uncaughtException]', err && err.stack || err);
|
||||
crashReports.writeCrashReport('main-uncaught-exception', { message: String(err && err.message || err), stack: String(err && err.stack || '') });
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[diag][main:unhandledRejection]', reason && reason.stack || reason);
|
||||
@@ -98,6 +101,10 @@ process.on('unhandledRejection', (reason) => {
|
||||
// child-process-gone fires for GPU/utility/renderer process deaths. The GPU one is especially useful: a GPU crash forces the renderer to recover its compositor, and that recovery can itself crash on Windows.
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
console.error('[diag][main:child-process-gone]', JSON.stringify(details));
|
||||
// Clean exits and user kills are not crashes; reporting them would bury the real ones.
|
||||
if (details && details.reason && details.reason !== 'clean-exit' && details.reason !== 'killed') {
|
||||
crashReports.writeCrashReport('child-process-gone', details);
|
||||
}
|
||||
});
|
||||
// Platform-split auto-updater: electron-updater on Mac (full-featured), Electron's
|
||||
// built-in autoUpdater on Windows (Squirrel.Windows target; electron-updater dropped Squirrel).
|
||||
@@ -1199,6 +1206,7 @@ function markBackendReady() {
|
||||
// Read lazily: mainWindow is replaced by recreateMainWindow, so a captured value goes stale.
|
||||
workflowsLifecycle.setNotificationTarget(() => mainWindow);
|
||||
workflowsLifecycle.startPolling();
|
||||
crashReports.init(app, (payload) => { try { workflowsLifecycle.showNativeNotification(payload); } catch (_) {} });
|
||||
} catch (_) {}
|
||||
try { connectMainBridge(); } catch (_) {}
|
||||
}
|
||||
|
||||
@@ -50,25 +50,30 @@ const GeneralInterface: React.FC<{
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('ui_font_scale', 'Text size', 'Interface', 'Scales all text across the app; layout stays intact.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Text size</Typography>
|
||||
<Typography sx={descSx}>Scales all text across the app. Layout stays intact.</Typography>
|
||||
<Box sx={rowSx} {...settingSelectAttrs('ui_font_scale', 'Text size', 'Interface', 'Scales all text across the app; layout stays intact.')}>
|
||||
<Typography sx={labelSx}>Text size</Typography>
|
||||
<Typography sx={{ ...descSx, mb: 1 }}>Scales all text across the app. Layout stays intact.</Typography>
|
||||
<Box sx={{ px: 1 }}>
|
||||
<Slider
|
||||
value={form.ui_font_scale ?? 1}
|
||||
onChange={(_, v) => setForm({ ...form, ui_font_scale: v as number })}
|
||||
min={0.8}
|
||||
max={1.35}
|
||||
step={0.05}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(v) => `${Math.round(v * 100)}%`}
|
||||
marks={[
|
||||
{ value: 0.8, label: 'Small' },
|
||||
{ value: 1, label: 'Default' },
|
||||
{ value: 1.35, label: 'Large' },
|
||||
]}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
'& .MuiSlider-markLabel': { color: c.text.tertiary, fontSize: '0.6875rem' },
|
||||
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.ui_font_scale ?? 1}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v) setForm({ ...form, ui_font_scale: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={0.8}>Tiny</ToggleButton>
|
||||
<ToggleButton value={0.9}>Small</ToggleButton>
|
||||
<ToggleButton value={1}>Default</ToggleButton>
|
||||
<ToggleButton value={1.1}>Large</ToggleButton>
|
||||
<ToggleButton value={1.2}>Larger</ToggleButton>
|
||||
<ToggleButton value={1.35}>Largest</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('voice_hold_to_talk', 'Dictation', 'Interface', 'Hold to talk, or tap to start and stop.')}>
|
||||
|
||||
@@ -409,6 +409,17 @@ class WebSocketManager {
|
||||
if (data.status === 'running' && session_id) {
|
||||
store.dispatch(trackAgentNotification(session_id));
|
||||
}
|
||||
// Native OS notification when an agent finishes while the user is elsewhere: workflows already had this; long chat tasks deserve the same "it's done" tap on both platforms. Sub-agents stay silent (their parent's finish is the story).
|
||||
if (data.status === 'completed' && session_id && document.hidden) {
|
||||
const p_sess2 = data.session ?? store.getState().agents.sessions[session_id];
|
||||
if (p_sess2 && !p_sess2.parent_session_id && p_sess2.mode !== 'browser-agent') {
|
||||
void (window as any).openswarm?.notify?.({
|
||||
title: 'Agent finished',
|
||||
body: (p_sess2.name && p_sess2.name !== 'Untitled' ? p_sess2.name : 'Your task is done.').slice(0, 200),
|
||||
deepLink: `openswarm://session/${session_id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// An AppAgent driving an app card announces itself only via this status event (no card_added like browsers), so light the app card here. Keyed by the parent chat like browser glows, so the same terminal fade below clears it.
|
||||
const p_sess = data.session;
|
||||
|
||||
Reference in New Issue
Block a user