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:
ZacharyZhang-NY committed 2026-04-20 08:13:48 -04:00
1 parent 7a9a6779b0
commit e74dafcd36
287 files changed
+59858 -10

No files matched your search

+1
View File
@@ -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
View File
@@ -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)) {
+7
View File
@@ -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);
}
+7
View File
@@ -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) {
+38 -5
View File
@@ -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;