Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
e13a332ac8 fix: narrow skills watcher ignores (#1074) (thanks @roshanasingh4) 2026-01-17 07:56:35 +00:00
Roshan Singh
6aaa302be5 Fix #1056: ignore heavy paths in skills watcher
On macOS, watching deep dependency trees can exhaust file descriptors and lead to spawn EBADF failures. The skills watcher only needs to observe skill changes, so ignore dotfiles, node_modules, and dist by default. Adds regression coverage.
2026-01-17 07:40:20 +00:00
7 changed files with 46 additions and 6 deletions

View File

@ -48,6 +48,7 @@
- Sessions: propagate deliveryContext into last-route updates to keep account/channel routing stable. (#1058)
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
- Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields.
- Skills: narrow skills watcher ignores to `.git` so hidden workspaces still refresh. (#1074) — thanks @roshanasingh4.
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
- Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes.
- Messages: include sender labels for live group messages across channels, matching queued/history formatting. (#1059)

View File

@ -50,7 +50,7 @@ test("process submit sends CR for pty sessions", async () => {
const processTool = createProcessTool();
const result = await execTool.execute("toolcall", {
command:
"node -e \"process.stdin.on('data', d => { if (d.includes(13)) { process.stdout.write('submitted'); process.exit(0); } });\"",
"node -e \"process.stdin.on('data', d => { if (d.includes(10) || d.includes(13)) { process.stdout.write('submitted'); process.exit(0); } });\"",
pty: true,
background: true,
});

View File

@ -1,4 +1,5 @@
const DSR_PATTERN = /\x1b\[\??6n/g;
const ESC = "\u001b";
const DSR_PATTERN = new RegExp(`${ESC}\\[\\??6n`, "g");
export function stripDsrRequests(input: string): { cleaned: string; requests: number } {
let requests = 0;

View File

@ -0,0 +1,31 @@
import { describe, expect, it, vi } from "vitest";
const watchMock = vi.fn(() => ({
on: vi.fn(),
close: vi.fn(async () => undefined),
}));
vi.mock("chokidar", () => {
return {
default: { watch: watchMock },
};
});
describe("ensureSkillsWatcher", () => {
it("ignores node_modules, dist, and .git by default", async () => {
const mod = await import("./refresh.js");
mod.ensureSkillsWatcher({ workspaceDir: "/tmp/workspace" });
expect(watchMock).toHaveBeenCalledTimes(1);
const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown };
expect(Array.isArray(opts.ignored)).toBe(true);
const ignored = opts.ignored as RegExp[];
expect(ignored.some((re) => re.test("/tmp/workspace/skills/node_modules/pkg/index.js"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/dist/index.js"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.git/config"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/.hidden/skills/index.md"))).toBe(false);
});
});

View File

@ -125,6 +125,13 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Cla
stabilityThreshold: debounceMs,
pollInterval: 100,
},
// Avoid FD exhaustion on macOS when a workspace contains huge trees.
// This watcher only needs to react to skill changes.
ignored: [
/(^|[\\/])\.git([\\/]|$)/,
/(^|[\\/])node_modules([\\/]|$)/,
/(^|[\\/])dist([\\/]|$)/,
],
});
const state: SkillsWatchState = { watcher, pathsKey, debounceMs };

View File

@ -677,15 +677,15 @@ export function registerHooksCli(program: Command): void {
for (const hookId of targets) {
const record = installs[hookId];
if (!record) {
defaultRuntime.log(chalk.yellow(`No install record for \"${hookId}\".`));
defaultRuntime.log(chalk.yellow(`No install record for "${hookId}".`));
continue;
}
if (record.source !== "npm") {
defaultRuntime.log(chalk.yellow(`Skipping \"${hookId}\" (source: ${record.source}).`));
defaultRuntime.log(chalk.yellow(`Skipping "${hookId}" (source: ${record.source}).`));
continue;
}
if (!record.spec) {
defaultRuntime.log(chalk.yellow(`Skipping \"${hookId}\" (missing npm spec).`));
defaultRuntime.log(chalk.yellow(`Skipping "${hookId}" (missing npm spec).`));
continue;
}

View File

@ -102,7 +102,7 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] {
export function resolveAttachmentKind(
attachment: MediaAttachment,
): "image" | "audio" | "video" | "unknown" {
): "image" | "audio" | "video" | "document" | "unknown" {
const kind = kindFromMime(attachment.mime);
if (kind !== "unknown") return kind;