The harmony stream router only recognized the analysis and final channels, so
gpt-oss's standard `commentary` channel (tool-call preambles / function-arg
bodies) was unhandled: the literal `<|channel|>commentary` marker, the
`to=functions.*` recipient, and the commentary body all leaked into the
visible answer. Add commentary to the marker regex + the suffix-hold table, and
route its body to thinking (only `final` is user-facing). Adds a regression
test (split-chunk + recipient + body), verified to fail without the fix.
* fix(email): close IMAP socket when connect/login fails (#3174)
_imap_connect opened a live socket via _open_imap_connection and then
called conn.login() with no try/finally, and _open_imap_connection called
conn.starttls() unguarded. When auth fails (e.g. an Office 365 app password
on an MFA-enabled tenant, #3174) or STARTTLS is rejected, the already-open
socket was orphaned. Every IMAP caller funnels through _imap_connect,
including the 30-minute _auto_summarize_poller, so a persistently
misconfigured account leaked one descriptor per pass toward FD exhaustion.
The previously merged leak fixes (#1325/#1330/#1423/#1530) only guard the
post-connect body and monkeypatch _imap_connect to succeed, so this
connect-time path was uncovered. Wrap login() and starttls() so a failure
calls conn.shutdown() (low-level close; logout() can't run pre-auth) before
re-raising. Adds two regression tests that fail without the guard.
* fix(email): guard MCP IMAP+SMTP connect-time leaks too (#3174)
Folds in the sibling connect-time leaks vdmkenny flagged on #3363, so the
whole connect-then-step leak class is closed in one place:
- mcp_servers/email_server.py::_imap_connect — guard starttls() and login();
close pre-auth with conn.shutdown() before re-raising.
- mcp_servers/email_server.py::_smtp_connect — guard starttls() and login();
SMTP has no shutdown(), so close with conn.close() (socket close, no QUIT).
Routes SMTP (_send_smtp_message) is already safe via 'with smtplib.SMTP(...)'.
Adds four regression tests (one per guard), verified to fail without the fix.
PDF uploads are stored as markdown wrappers with pdf_source or pdf_form_source markers so the editor can preserve extracted text, form fields, and annotations. The library exposed that internal wrapper: auto-created PDF documents used the hashed storage filename as the title, and row/facet language reported markdown instead of pdf.
Derive chat-upload PDF titles from the original upload name, derive document-library display language from the PDF source marker for rows, filters, and facets, and keep markdown wrappers excluded from the markdown facet when they represent PDFs.
The expanded library card already renders PDF-backed documents through /api/document/{id}/render-pdf. Allow only that inline PDF preview endpoint to be framed by same-origin app pages while leaving normal routes on X-Frame-Options: DENY and frame-ancestors none.
Also tighten the existing PDF marker regression assertion so it matches the actual historical corruption signature instead of contradicting the preserved [Page 1 text]: marker.
Fixes#2468
The document editor stores the AI-edit diff state (_diffModeActive,
_diffOldContent, _diffNewContent, _diffChunks) as a module-global
singleton bound to whatever document was active when the diff opened,
and every document shares one #doc-editor-textarea. When the active
document is switched while an unapproved diff is open, the stale diff
must be discarded first or a later exitDiffMode (tab switch /
Accept-Reject-All) flushes the old document's content into the new
active document and overwrites it (issue #2467).
Guard both paths that switch the active document for an AI update,
while activeDocId still points at the previously-active doc:
- handleDocUpdate(): a doc_update targeting a different document.
- streamDocOpen(): the AI streaming a NEW document — this runs first on
that path, so a guard only in handleDocUpdate would fire too late and
still overwrite the streamed document.
Both reuse the exact `if (_diffModeActive) exitDiffMode(true);` guard
switchToDoc() and enterDiffMode() already use.
Fixes#2467
get_context_length() cached the resolved context window by model id alone,
so two different remote endpoints serving the same model id (e.g. a capped
proxy at 8k vs. the full provider at 200k) collided: the first to resolve
won process-wide and the other endpoint was served the wrong window. That
silently over-trims conversations on the larger-window endpoint (it feeds
context_compactor) or overflows the smaller one (provider 400s).
Key the cache on (endpoint_url, model). Local endpoints already always
re-query, so they are unaffected.
Fixes#2603
MCP server tools were presented to the agent with only their name and a
truncated description: get_tool_descriptions_for_prompt() emitted
"- name: description" and get_all_tools() dropped input_schema entirely.
On the fenced-block tool path (used by Ollama models), the agent could
not see a tool's declared inputs and guessed argument names from the
description alone, so tool calls failed (issue #2509). MCP inspector
showed the schemas fine, confirming the loss was on our side.
- get_all_tools() now carries each tool's input_schema.
- get_tool_descriptions_for_prompt() renders a compact args hint
(parameter names, coarse types, required-ness) via a new
_format_mcp_params() helper, matching the "Args (JSON): {...}" style
the built-in tool descriptions already use.
Fixes#2509