Skip to content

app: each new chat keeps ~10 files open in the engine for the life of the app (macOS soak) #2471

Closed

Description

@AbirAbbas

Measured on macOS 15.6 / Apple Silicon during a 58-minute soak (2026-11-06). Engine: app/v3 3a6aa31 + #2466. Desk: main 0f1974872.

What happens: each new chat leaves codeaf app holding about 10 more open files for as long as the app runs. The files are the project directory, .cell, transcript.jsonl, meta, lock, presence, card and told files. The seeded long chat holds 7.

  • During the steady phase, three tool turns each ran in a new chat. Engine fds went 59 → 74 → 85 → ~96, flat between turns.
  • Over the same phase, per 10 min: engine footprint +5.6 MB, threads +0.5, wakeups +1.7/s.
  • Desk processes (app, WebContent, WebKit GPU) stayed flat over the same phase.

Likely cause: a chat whose window is open is never released (app.go:79-82). Each chat also keeps its presence heartbeat (5 s / 7 s, session/taskpresence.go). On macOS, fsnotify's kqueue backend holds a handle per watched file in each session folder.

Why it matters: growth is bounded by the number of chats opened in a session, not by time. A long-lived app that opens many chats keeps accumulating fds and per-chat timers.

Possible direction: close an idle chat's files and watches when it isn't shown and has no live turn. Keep the presence heartbeat, which #2065 keeps on purpose; only the watch and the open files would go.

Raw data: soak-60s.jsonl and soak-turn-{1,2,3}.jsonl from the push (sampler: libproc rusage per pid).

🤖 Generated with Claude Code

Activity

  1. AbirAbbas commented on Nov 9, 2026

    @AbirAbbas
    CollaboratorAuthor

    Plan for this issue (2026-11-09): one engine PR on app/v3 with a macOS-only folder watch.

    • Cause of most of the growth: internal/session/presence_watch.go gives each held chat's session folder to fsnotify, and fsnotify's macOS (kqueue) backend opens one file descriptor for every file in a watched folder. Linux and Windows use one handle per folder. The watch only needs to see request files appear (takeover.json, .conversation-delete-request, .task-delete-request-*), and those are published by rename into the folder, so a watch on the folder's own handle is enough.
    • What stays after it: any file a held chat keeps open for its own use, outside the watch, stays open while the chat is held. Closing those needs the engine to know a chat is no longer shown. agents.close can't be that signal: it is the person's Close (it marks the chat Closed and stops its services). The fix needs a new view-closed call plus desk wiring, which is a separate change across two repos. The Mac before/after on this PR will name exactly which files remain per chat.
    • Not changed: holds, viewOpen and the idle sweep. Releasing held chats in Let idle desktop chat views beyond the three most recent go #2472 broke live turns started by another process (reverted in Revert: let idle desktop chat views beyond the three most recent go #2473).
  2. AbirAbbas commented on Nov 9, 2026

    @AbirAbbas
    CollaboratorAuthor

    Fixed by #2551 (merged into app/v3 as 0dc2bd1).

    On macOS the presence watch now holds one kernel handle per held chat's folder, plus one for answers.jsonl only while an answer waits. Before, fsnotify's kqueue backend opened a handle for every file in the folder.

    Mac proof (macOS 15.6, codeaf app driven over its wire, 10 held chats): engine open files per held chat went from +6 to +2. Takeover reaction is unchanged at 1–3 ms, and everything is released when the chats close.

    What stays per held chat is its journal's write handle and the one folder watch. That is the cost of holding a chat, not a leak. A desk "view closed" call could release idle held chats, but that would be a separate optimization with no measured problem behind it today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions