Silence render-disposed spam, unbreak bundled skills, drop board wake
Three independently ugly bugs that all fed the same log flood:
* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
to stderr via `console.error`. When the worker fans a second
agent-event past a reloading renderer, we produced thousands of
identical lines and stalled the main loop on stderr I/O. Added a
`mainFrame.detached` guard so the send is short-circuited before
Electron's own catch block fires, and installed a targeted filter in
`suppress-warnings.ts` (now actually imported from the entry point)
so the specific `console.error("Error sending from webFrameMain:"…)`
noise never reaches the terminal. Any other payload still surfaces.
* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
target when an agent replied to `__board__` — `wakeAgentIfPossible`
then called `db.getAgent("__board__")`, threw, and the dispatcher
kept repeating on every send. Strip `HUMAN_SENDER_ID` at
`resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
heartbeat so a stray caller (future automation fan-out) cannot
reintroduce the crash.
* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
diverges from its parent directory. The four bundled `my-writing-style`
copies declared `name: writing-style-skill`, so every install emitted
a warning and dropped the skill. They also lived as orphan gitlinks
(mode 160000 without .gitmodules), and the global `*.md` gitignore
hid every other SKILL.md from version control. Fixed the frontmatter
to match the dir, converted the dangling submodules to plain trees,
and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
catalogue instead of silently losing it.
This commit is contained in:
1 parent
7a9a6779b0
commit
e74dafcd36
287 files changed
+59858
-10
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import "./suppress-warnings";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { app } from "electron";
|
||||
|
||||
+12
-1
@@ -96,8 +96,19 @@ export function createIpcSupport({
|
||||
function emitEvent(event: DesktopEvent) {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
const webContents = mainWindow.webContents;
|
||||
if (webContents.isDestroyed() || webContents.isCrashed()) return;
|
||||
// `mainFrame.detached` flips true while the frame is being torn down
|
||||
// or swapped during a navigation. Sending into a detached frame is
|
||||
// what makes Electron's native layer throw "Render frame was
|
||||
// disposed" and also what makes its own WebFrameMain.prototype.send
|
||||
// spam `console.error('Error sending from webFrameMain: ', …)` to
|
||||
// stderr on every call. Short-circuiting here closes the window
|
||||
// between our `isDestroyed` check and the native send.
|
||||
const mainFrame = webContents.mainFrame;
|
||||
if (!mainFrame || mainFrame.detached) return;
|
||||
try {
|
||||
mainWindow.webContents.send("agent-company:event", event);
|
||||
webContents.send("agent-company:event", event);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes(renderFrameDisposedMessage)) {
|
||||
|
||||
@@ -260,6 +260,13 @@ export function resolveMessageWakeTargets(
|
||||
}
|
||||
}
|
||||
|
||||
// The human participant (`__board__`) is a reader, not a wake-able
|
||||
// agent: it has no runtime, no heartbeat, no DB row. Leaving it in the
|
||||
// set makes `wakeAgentIfPossible` call `db.getAgent("__board__")`,
|
||||
// which throws "Agent not found" and spams the log. Filter once, at
|
||||
// the point of resolution, so every downstream consumer is clean.
|
||||
wakeTargets.delete(HUMAN_SENDER_ID);
|
||||
|
||||
return Array.from(wakeTargets);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AgentRecord, ConnectorRecord } from "@shared/types";
|
||||
import type { OrchestrationDependencies } from "./types";
|
||||
import type { RunDispatchParams } from "./turn-context";
|
||||
import { pickModelForAgent } from "../model-dispatch";
|
||||
import { HUMAN_SENDER_ID } from "../message-dispatch";
|
||||
|
||||
/**
|
||||
* Every reason a heartbeat attempt can be rejected before a run is
|
||||
@@ -258,6 +259,12 @@ export function createHeartbeat(
|
||||
|
||||
function wakeAgentIfPossible(agentId: string | null | undefined, companyId: string, trigger: string, context?: Record<string, unknown>) {
|
||||
if (!agentId || isQuitting()) return null;
|
||||
// `__board__` is the human participant sentinel, not an agent row.
|
||||
// Upstream filters should strip it before we get here, but keeping
|
||||
// this guard as defense-in-depth prevents a single stray call site
|
||||
// (e.g. a future automation action that forwards the `toAgentId`
|
||||
// raw) from spamming the log with "Agent not found: __board__".
|
||||
if (agentId === HUMAN_SENDER_ID) return null;
|
||||
try {
|
||||
const agent = db.getAgent(agentId);
|
||||
if (agent.companyId !== companyId) {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
/**
|
||||
* Silences the `node:sqlite` ExperimentalWarning emitted on first import.
|
||||
* Must be imported before any module that touches `node:sqlite`, otherwise
|
||||
* the warning has already fired.
|
||||
* Silences two Electron/Node sources of stderr spam that are pure
|
||||
* lifecycle noise, not logic bugs:
|
||||
*
|
||||
* Targeted: only the SQLite experimental warning. All other warnings pass
|
||||
* through unchanged.
|
||||
* 1. `node:sqlite` ExperimentalWarning on first import — emitted by the
|
||||
* Node runtime itself. Must be installed before any module touches
|
||||
* `node:sqlite`, or the warning has already fired.
|
||||
*
|
||||
* 2. `Error sending from webFrameMain:` — emitted by Electron's own
|
||||
* `WebFrameMain.prototype.send` when the renderer frame was disposed
|
||||
* mid-send (window close, dev HMR, reload). Electron catches the
|
||||
* native exception internally and `console.error`s it; the send is
|
||||
* already a no-op by the time our `webContents.send` returns, and
|
||||
* nothing downstream reads the message. With every agent-event fan
|
||||
* out producing two sends, a single dead renderer floods the log
|
||||
* with thousands of copies and can stall the main event loop on
|
||||
* synchronous stderr I/O. We filter only this specific prefix so
|
||||
* real console errors still surface.
|
||||
*
|
||||
* Targeted: everything else passes through unchanged.
|
||||
*/
|
||||
|
||||
const originalEmitWarning = process.emitWarning.bind(process);
|
||||
@@ -23,3 +36,23 @@ process.emitWarning = ((warning: unknown, ...rest: unknown[]) => {
|
||||
if (isSqliteExperimentalWarning(warning)) return;
|
||||
return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest);
|
||||
}) as typeof process.emitWarning;
|
||||
|
||||
const originalConsoleError = console.error.bind(console);
|
||||
|
||||
function isDisposedWebFrameNoise(args: unknown[]): boolean {
|
||||
if (args.length === 0) return false;
|
||||
const first = args[0];
|
||||
if (typeof first !== "string") return false;
|
||||
if (!first.startsWith("Error sending from webFrameMain")) return false;
|
||||
// Only swallow when the accompanying error is the disposal signal;
|
||||
// any other payload surfaces normally so genuine IPC failures still
|
||||
// show up in logs.
|
||||
const payload = args[1];
|
||||
const message = payload instanceof Error ? payload.message : String(payload ?? "");
|
||||
return message.includes("Render frame was disposed");
|
||||
}
|
||||
|
||||
console.error = ((...args: unknown[]) => {
|
||||
if (isDisposedWebFrameNoise(args)) return;
|
||||
return originalConsoleError(...args);
|
||||
}) as typeof console.error;
|
||||
Reference in new issue
Block a user