Skip to content

Several open() calls without context manager — possible FD leak in long-running bot daemons #96

@shaun0927

Description

@shaun0927

Background

PR #76 fixed one open(...).read() instance in reflect/scheduler.py by
wrapping it in with. Quick grep over the rest of the codebase finds
several similar patterns that rely on garbage collection to close the
handle:

ga.py:23     if os.path.exists(cr_header): tmp_file.write(open(cr_header, encoding='utf-8').read())
ga.py:387    old = open(path, 'r', encoding=\"utf-8\").read() if os.path.exists(path) else \"\"
ga.py:388    open(path, 'w', encoding=\"utf-8\").write(new + old)
agentmain.py:16   open(...).read()      # tools_schema load
agentmain.py:24   open(mem_txt, 'w').write(...)
agentmain.py:28   open(...).write(open(...).read())   # nested, two handles
agentmain.py:236  open(...).<append>     # log file
frontends/wechatapp.py:183  open(p, 'wb').write(pt)

For the launchable Streamlit/CLI flow these eventually get GC'd and the
process exits anyway, so the existing behavior is mostly fine. But
tgapp.py, wechatapp.py, fsapp.py etc. run as long-lived daemons,
and several of these patterns are inside per-message handlers. On
default Linux ulimit (1024 FDs) a busy bot will eventually trip
OSError: [Errno 24] Too many open files.

This may also be the root cause of #48 ("multiple calls then model
unresponsive, needs restart") — the symptom there matches FD
exhaustion: the agent stops responding mid-conversation and a restart
clears it.

Proposal

Single janitor-style PR that converts all of the above to
with open(...) as f:. Net line count is +0 (extra with line is
balanced by removing the chained call). Same byte intent, no behavior
change for the happy path.

Two questions before I send the PR:

  1. Do you prefer a single janitor PR touching ~5 files, or split per
    file? CONTRIBUTING.md asks for "small change radius" — I read this
    as "don't ripple A → B → C", which the FD-hygiene change does not
    do (each file is independent). But happy to split if you prefer.

  2. There are also a few open(...).read() calls inside helper closures
    that get called once per process startup (e.g. _load_mykeys in
    llmcore.py). Should those stay as-is to keep the boot path
    compact, or get the same with treatment for consistency?

Reference: PR #76 already established the with-context pattern as
acceptable in this codebase, so this is a follow-up applying the same
pattern uniformly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions