MAY 27 2026 / 116 COMMITS / 6 PATCH RELEASES

New AI projects on Hugging Face or GitHub, May 27 2026. One repo shipped the precision replacement for yesterday's deletion.

The day before, the open-source macOS agent Fazm removed a 600-second blunt timeout. The argument was that a clock cannot tell a slow model apart from a stuck one. The argument only holds if something else does. On May 27 the same repo shipped that something else, in two commits totaling 171 lines and one regular expression: a 15 second per-tool stall detector and a SIGKILL escape hatch that walks the process tree to find the actual wedged Playwright subprocess. This is that change, read in the commits and the release ledger.

M
Matthew Diakonov
9 min read

Direct answer, verified 2026-05-29

No platform publishes an official "May 27, 2026" list of new AI projects. Both Hugging Face and GitHub order discovery by a rolling trending score, not by calendar date. New work surfaces continuously across three live feeds:

For one record carrying that exact date: the open-source macOS agent Fazm pushed 116 commits on 2026-05-27 and cut six patch releases between 14:14 and 19:25 Pacific. The standout shipment, v2.9.45, was the precise replacement for the previous day's deleted blunt timeout, a 15 second per-tool stall detector and a SIGKILL force-stop that walks the process tree for wedged Playwright subprocesses. It is recorded in CHANGELOG.json at the root of the repository. The rest of this page opens that shipment.

Yesterday set up the problem

The previous day's piece walked through commit 787dbb33: 45 lines moved in ACPBridge.swift to delete a single constant called inactivityTimeout and the two waitForMessage call sites that passed it. The argument in the comment was protocol-level. The Agent Client Protocol defines exactly five ways a prompt turn can end and Timeout is not one of them. A blunt clock that fires on output silence cannot distinguish a slow model from a stuck process; a clock that fires anyway penalizes correctness.

That argument leaves an obvious hole. Real hangs happen. A Playwright MCP attached to a Chrome extension whose tab has died will sit on its tool call forever, ignoring the cooperative session/cancel that ACP defines. If the blunt timeout is the wrong answer, what is the right one? May 27 is the day that question got answered, in two commits, by one author, in a single file.

One day, six releases, in order

Walk the day top to bottom. The morning work is rescue. The afternoon work is the force-stop landing. The evening work is the cleanup the force-stop made obvious.

1

Morning: onboarding rescue work

09:36 to 11:13 PDT, 12 commits clear three stuck-onboarding traps: salvaging orphaned assistant messages (079186e5), preventing onboarding sessions from leaking into main-chat persistence (aef80d99), and a Retry/Skip banner for empty turns (189d723a).

Shipped as v2.9.43 at 14:14 PDT.
2

Midday: streaming and scroll

11:46 to 13:48 PDT, the chat-rendering layer gets reworked: ScrollPositionDetector logs why it left bottom (7e901c00), AIResponseView gets virtualization for off-viewport responses (6dce5477) and a 20Hz flush cap on streaming (bdff4746). The detached chat scroll-detach bug is fixed.

Shipped as v2.9.44 at 17:21 PDT.
3

Early afternoon: idle finalization

15:12 to 15:14 PDT, e8dbe95c adds idle finalization to rescue hung session/prompt requests and 81630044 handles FINALIZATION_IDLE errors. The detached-chat spinner can no longer stay stuck forever; a safety watchdog re-arms when the turn quietly completes.

Foundation for the force-stop work that follows.
4

Mid-afternoon: the stall detector lands

15:33 PDT, commit 9ce9a77a adds 56 lines to acp-bridge/src/index.ts implementing the per-tool stall detector. STALL_THRESHOLD_MS is 15 seconds; the detector ticks every 5 seconds, flags any mcp__* tool whose lastUpdateAt is older than the threshold, and emits tool_stalled to the UI. It does not cancel anything; the turn keeps running.

Companion UI commit b80f9f21 adds the live indicator to AIResponseView.
5

Mid-afternoon: the SIGKILL escape hatch

16:12 PDT, commit bd4bb69e adds 115 lines to the same file implementing force-interrupt. It walks the descendant pids of acpProcess.pid via `pgrep -P`, reads each child's command line via `ps -p <pid> -o command=`, keeps every match for /playwright/, and sends SIGKILL. After the kill, the SDK's MCP client sees the transport drop and the in-flight tool errors instead of hanging.

Followed by ten UI commits wiring the Force stop button and the Retry card. Shipped as v2.9.45 at 19:25 PDT.
6

Evening: layout-storm cleanup

19:11 to 19:13 PDT, the team kills a separate scroll-storm bug: e8bc9098 fixes an animation leak in FloatingControlBarView, 21a9cb20 switches to value-based animations, and 80d92901 adds auto-sampling for sustained hot threads to the resource monitor. v2.9.46 ships at 19:25 PDT, four minutes after v2.9.45.

Total: six patch releases in a window of seven hours, twelve minutes.
7

Late evening: external LLM tracing

20:33 to 20:41 PDT, a nine-commit cluster wires LLM usage reporting for Fazm-paid turns: token tracking added to the Codex and Gemini query paths (d22f6051, 0fef608d), model attached to query results (58ef859f), and external traces recorded per turn (9fc1990d).

Lands the next morning in v2.9.47 at 03:07 PDT.

The stall detector, in the file the author edited

Commit 9ce9a77a, 15:33 PDT, adds 56 lines to acp-bridge/src/index.ts. Two constants and one setInterval. The constants are the spec this whole change works to:

acp-bridge/src/index.tscommit 9ce9a77a, +56 / -0
// --- Stall detector ---
// Surface (but do NOT cancel) an in-flight `mcp__*` tool that has stopped
// emitting status updates for more than STALL_THRESHOLD_MS. A healthy slow
// tool keeps emitting in_progress / rawInput updates (lastUpdateAt
// advances); a wedged SDK→MCP forward goes silent (Playwright on a dead
// Chrome extension is the canonical case).
const STALL_THRESHOLD_MS = 15_000;
const STALL_CHECK_INTERVAL_MS = 5_000;
const stalledTools = new Map<string, { sessionId: string | undefined }>();

setInterval(() => {
  const now = Date.now();
  for (const [toolCallId, tool] of inFlightTools) {
    if (!tool.title.startsWith("mcp__")) continue;
    const silentMs = now - tool.lastUpdateAt;
    if (silentMs > STALL_THRESHOLD_MS && !stalledTools.has(toolCallId)) {
      stalledTools.set(toolCallId, { sessionId: tool.sessionId });
      sendWithSession(tool.sessionId, {
        type: "tool_stalled",
        stalled: true,
        toolName: tool.title,
        toolUseId: toolCallId,
        elapsedSeconds: (now - tool.startedAt) / 1000,
      });
    }
  }
  // resumed-or-gone tools clear via the same shape with stalled: false
}, STALL_CHECK_INTERVAL_MS).unref();

Three small choices in those constants do most of the work.

  • 15 seconds, not 60. Most MCP tools that work emit at least an in-progress update every second or two. A genuine wedge is silent for tens of seconds before anyone notices. 15 is well above the noise floor and well below the "is this thing on" threshold a user feels.
  • Per-tool, not per-turn. The map key is toolCallId; the "silence" that triggers the flag is silence on one specific tool call, not the whole turn. A slow model that is between tool calls does not trip anything.
  • Informational, not enforcing. The detector emits a message and returns. It does not abort the query, does not call session/cancel, does not touch the subprocess. It only tells the UI what it sees.

The SIGKILL escape hatch, in one regex

Commit bd4bb69e, 16:12 PDT, adds 115 lines to the same file. The top half implements a process-tree walker. The bottom half uses it to find every descendant of the codex-acp parent whose command line matches a regex, and SIGKILLs them. The matching pattern is /playwright/. That is the whole filter. Everything is built on top of it.

acp-bridge/src/index.tscommit bd4bb69e, +115 / -0
function findDescendantsMatching(
  parentPid: number,
  cmdPattern: RegExp,
): Array<{ pid: number; command: string }> {
  const results: Array<{ pid: number; command: string }> = [];
  const visited = new Set<number>();
  function walk(pid: number) {
    if (visited.has(pid)) return;
    visited.add(pid);
    const out = execSync(`pgrep -P ${pid}`, { encoding: "utf8" }).trim();
    const children = out ? out.split("\n").map(Number) : [];
    for (const c of children) {
      const cmd = execSync(`ps -p ${c} -o command=`, { encoding: "utf8" }).trim();
      if (cmdPattern.test(cmd)) results.push({ pid: c, command: cmd });
      walk(c);
    }
  }
  walk(parentPid);
  return results;
}

function forceStopBrowserMcps(reason: string): { killedPids: number[] } {
  if (!acpProcess?.pid) return { killedPids: [] };
  const matches = findDescendantsMatching(acpProcess.pid, /playwright/);
  const killedPids: number[] = [];
  for (const { pid } of matches) {
    process.kill(pid, "SIGKILL");
    killedPids.push(pid);
  }
  return { killedPids };
}

Two trade-offs are documented in the comments. The first is honest about blast radius: the SDK does not expose which Playwright PID belongs to which session, so a force-stop in chat A may kill a Playwright subprocess that chat B was using. The author accepts that because the extension shares one Chrome anyway. The second is why this works at all. Once the subprocess dies, the SDK's MCP client sees the JSON-RPC transport close. The in-flight tool call returns an error to the agent loop instead of hanging forever, and the turn unwinds normally. The kill is what makes cancellation actually arrive.

Slow success vs. real failure, side by side

The argument for replacing the blunt timeout with these two mechanisms only lands if the new instruments draw a clean line where the old one drew a smudge. They do. The signals are different, the timing is different, and the user-visible behavior is different.

A slow model mid-answer

Gemini Pro is six minutes into producing a long, correct refactor. Token stream is sparse. No tool calls in flight.

  • inFlightTools is empty.
  • tool_stalled is never emitted.
  • No indicator appears. No Force stop button.
  • Turn runs to its real StopReason.
A wedged browser tool

A mcp__playwright__browser_click is in flight against a Chrome extension whose tab is dead. The MCP forward is silent.

  • One entry in inFlightTools, mcp__ prefix.
  • At 15 seconds, tool_stalled fires, "Browser is not responding" appears.
  • User clicks Force stop. SIGKILL walks the tree.
  • Retry card lands. Conversation intact, prompt re-sendable.

Six releases, in seven hours

Five other releases hit the App Update channel between v2.9.41 and v2.9.46 the same day. None of them carried the force-stop work, but every one of them is a window into how the project treats shipping: a release is the unit of one named fix, not the unit of a marketing decision.

v2.9.4114:14Released the previous day's timeout deletion. The 10-minute inactivity cap on AI replies came out.
v2.9.4214:14Claude account picker no longer hangs when re-auth is needed and the keychain entry has gone stale.
v2.9.4314:14Onboarding chat no longer goes silent after a restart. Invalid Custom API Endpoint now falls back to default instead of erroring.
v2.9.4417:21Chat scroll stays pinned during streaming. Streaming flush rate capped at 20Hz to drop CPU usage in long conversations.
v2.9.4519:25Force-stop trilogy: 15-second stall indicator, SIGKILL escape hatch for wedged Playwright MCPs, Retry card that re-sends the prompt. Session-recording flush fix.
v2.9.4619:25Rate-limited auto-scroll and value-based animations to kill the scroll-and-layout storm seen during long streaming responses.

v2.9.45, the force-stop release, is highlighted. Cut times are Pacific, sourced from the corresponding "Release changelog for v..." commits in the public log.

Why the previous day's argument needed today's code

The May 26 deletion was defensible at the protocol layer. ACP has no Timeout stop reason; a loop-wide wall clock invents one and penalizes the only signal it has, which is total output silence. A slow model goes silent for the same reason a stuck process does: no bytes on the pipe. The blunt cure could not tell them apart.

But removing the cure without a replacement leaves the actual stuck case worse, not better. A Playwright tool call that hangs against a dead Chrome extension would simply hang forever, because ACP's session/cancel is cooperative and the wedged call has no event loop to receive it. The May 27 work closes that gap by looking at the right signal: not loop-wide silence, but per-tool silence. A tool call that has stopped emitting updates for longer than 15 seconds while everything else is fine is a much better wedge detector than total output silence on a 600-second clock.

And when the wedge is real, the kill is unilateral. There is no polite Stop sent to the subprocess. The SDK does not expose a sane handle. The process tree gets walked, the regex gets matched, the kill arrives. Once the JSON-RPC pipe drops, the in-flight tool errors back to the agent and the turn unwinds. The two commits together are the answer to a question the previous day opened: what should happen instead of a clock.

How to read any project's real May 27

The same recipe works against any other GitHub project that catches your eye. Open the changelog. Read the entries dated 2026-05-27. Pick the one whose headline sounds load-bearing. Open the commit log between midnight and midnight on that date, ignore the merges, and find the commit whose subject matches the headline. Run git show --stat <hash> to see which file moved and by how much. If you only read one file, read the one with the largest delta. If you only read one section of that file, read the comment that introduces the change, not the commit message; the comment is what the author wrote for future readers, the commit message is what they wrote for the history.

A daily roundup tells you what existed at the end of the day. A commit log tells you what the maintainer was thinking, hour by hour, in the order they thought it. The two are not interchangeable. On May 27 the maintainer was thinking about how to replace a timeout with something that watches the right thing. The roundups will not show you that. The commits and the file will.

Bring a repo you are evaluating

In twenty-five minutes we open its changelog, the commit log for one day, and one diff together, and decide whether the project earns a place in your stack.

Frequently asked questions

What new AI projects appeared on Hugging Face or GitHub on May 27, 2026?

Neither platform publishes a dated release list. Models, datasets, papers, and repositories surface continuously, ordered by a rolling trending score rather than by calendar date. The three live feeds worth watching are huggingface.co/models sorted by trending, huggingface.co/papers for research with linked implementations, and github.com/trending for the application layer. The closest thing to a true daily record is the commit log, where each change has an author, a message, and a timestamp to the second. As one record carrying that exact date, the open-source macOS agent Fazm pushed 116 commits on 2026-05-27 and cut six patch releases between 14:14 and 21:56 Pacific (v2.9.41 through v2.9.46). The standout shipment, in v2.9.45, was the precise replacement for the previous day's deleted blunt timeout: a 15-second per-tool stall detector, a SIGKILL force-stop that walks the process tree for wedged Playwright subprocesses, and a Retry card that re-sends the prompt that was in flight.

Why does this matter if I do not use Fazm?

The mechanism generalizes. Anyone building on the Model Context Protocol eventually meets the same failure mode: an MCP tool call that stops responding while the bridge subprocess that hosts it is still alive, so the cooperative cancel goes unanswered. A wall-clock timeout cannot tell that situation apart from a slow-but-correct model. Fazm's day shows a working answer in 116 commits: watch per-tool update times, not turn-wide silence; surface the silence to the user at 15 seconds instead of guessing at 10 minutes; let the user escalate from a soft cancel to a SIGKILL on the specific subprocess that is wedged. Whatever your stack is, those three pieces are the right shape.

What is the literal threshold and check rate for the stall detector?

Two constants near the top of the stall-detector block in acp-bridge/src/index.ts: STALL_THRESHOLD_MS = 15_000 and STALL_CHECK_INTERVAL_MS = 5_000. The detector wakes every 5 seconds, walks the inFlightTools map, and flags any tool whose title starts with mcp__ that has been silent longer than 15 seconds. It emits a tool_stalled message with stalled: true, the tool name, the tool-use id, and elapsedSeconds. When the tool resumes (or leaves inFlightTools at all) it emits the same shape with stalled: false. It does not cancel anything; the turn keeps running. The 300-second per-tool watchdog still sits underneath for the case where no human is watching.

How does the force-stop actually find the wedged subprocess?

It walks the process tree under the codex-acp parent, pid by pid, using `pgrep -P <pid>` to list children and `ps -p <pid> -o command=` to get each child's command line, then keeps every command that matches the regex /playwright/. Every match is sent SIGKILL. The implementation is in a function called forceStopBrowserMcps in acp-bridge/src/index.ts; the search helper is findDescendantsMatching. The comment in the source is honest about the trade-off: the SDK does not expose per-session MCP PIDs, so a force-stop in one session can kill Playwright subprocesses belonging to another. The author accepts that because a stalled Playwright is sharing one Chrome via the extension anyway. The code is a single 115-line patch, commit bd4bb69e.

What does the user actually see when this fires?

Three things, in order. First, once a Playwright tool has gone silent for 15 seconds, a live indicator appears next to the streaming response: 'Browser is not responding · Ns', counting up. Second, an affordance to escalate Stop into Force stop. Third, if the user clicks Force stop, a system event card slides into the chat naming the tool that was killed ('mcp__playwright__browser_click' or similar) with a Retry button. Clicking Retry re-sends the prompt that was in flight, so the user does not have to retype what they just asked. The chat state, including every earlier message, stays intact: the force-stop kills the subprocess, not the session cache.

Why six releases in one day?

Because each release was scoped to a small, named fix and the team treats versioning as a record-keeping device, not a marketing event. v2.9.41 cleared the menu-bar dead-click on launch-at-login and dropped the 10-minute inactivity cap (the commit landed the day before, the release shipped this day). v2.9.42 unhooked a Claude-account picker that hung on expired keychain entries. v2.9.43 fixed onboarding chat going silent after a restart and a custom-API-endpoint validation bug. v2.9.44 stopped chat scroll from detaching mid-stream. v2.9.45 carried the force-stop trilogy and a session-recording flush fix. v2.9.46 closed the day with rate-limited auto-scroll and value-based animations to kill a layout storm in long streaming responses. The pattern is six small landings instead of one rolled-up note; the commit log is the unit of work.

How do I read any project's real May 27 myself?

Same recipe as the previous day's piece, in three artifacts. Open the project's CHANGELOG.json (or CHANGELOG.md) at the root and read the entries dated 2026-05-27. Open the commit log via `git log --since=2026-05-27 --until=2026-05-28` and read the messages, ignoring merges. Then pick one commit whose subject sounds load-bearing and run `git show --stat <hash>` to see which file moved and by how much. The README is marketing; the diff is the truth. Deletions matter as much as additions, especially for safety mechanisms. The bigger the project, the more you can learn from one carefully read commit than from a roundup of fifty.

Where do I find the actual files this page is citing?

github.com/mediar-ai/fazm is the public repo. The stall detector lives in acp-bridge/src/index.ts in the setInterval block that opens with the comment '--- Stall detector ---' (search for STALL_THRESHOLD_MS). The force-stop functions are in the same file: findDescendantsMatching walks the process tree and forceStopBrowserMcps sends SIGKILL. The UI side is in Desktop/Sources/Components/FloatingControlBar/AIResponseView.swift (the 'Browser is not responding' indicator and the Force stop button) and in DetachedChatWindow.swift. Release notes are in CHANGELOG.json at the repo root.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.