Commit Graph

47942 Commits

Author SHA1 Message Date
kshitijk4poor
f7bbbc474e fix(plugins): abandon only nested plugin loads still running at outer timeout
Gate finding BN10.r1.2c (Low): the inline re-entrant branch appended the
nested ctx to the outer load's scope and never removed it, so a nested
load that had already finished (and was recorded enabled under its own
key) got fenced by _abandon_load() when the outer load later timed out.
Keep the ctx in scope only while its fn() runs.

The kept nested-inline test now times the outer load out with one finished
and one hung nested load, asserting only outer + hung are abandoned; this
also pins the nested-abandon behaviour of the previous commit.
2026-10-03 16:53:54 +05:30
kshitijk4poor
0e77d05974 fix(gateway): don't start sibling deferred loads from a nested registry walk
Gate finding BN10.r1.2c (High): the per-flow _loading check only stopped a
nested walk from waiting on its own parent key. A register() that re-walks
the registry on its deadline worker could still pop a SIBLING deferred
loader, whose _loader then blocks on PluginManager._discovery_lock held by
the outer walk while it joins that same worker -> PluginLoadTimeout and the
outer plugin is abandoned. A flow already inside a loader now returns
before popping a new loader; the outer walk resolves the siblings itself.

The registry test's loaders now hold a shared RLock like the real _loader,
with two deferred platforms, so it pins the sibling case (red without fix).
2026-10-03 16:53:54 +05:30
kshitijk4poor
5f3e288918 fix(plugins): abandon nested inline plugin contexts on outer timeout
Nested loads now run inline on the outer deadline worker, so their own
deadline branch never runs: if the outer load timed out, only the outer
PluginContext was abandoned and a nested context stayed live for late
registrations from the stranded worker. Hold the outermost load's contexts
in the ContextVar and abandon all of them on timeout.

Co-authored-by: JerryLiu369 <953639086@qq.com>
2026-10-03 16:53:54 +05:30
kshitijk4poor
e95ca8c3ae test(plugins): drop marker-visibility test kept by the nested-inline test
The marker test passes counterfactually (any worker-local flag satisfies it);
test_nested_plugin_load_runs_inline_on_deadline_worker covers the headline
behaviour and goes red on BASE.
2026-10-03 16:53:54 +05:30
kshitijk4poor
ce376877d9 fix(gateway): track deferred-platform load ownership per flow, not thread
A plugin's register() runs on a plugin-load deadline worker. When it re-walks
the platform registry (i18n/YAML layering -> plugin_entries()), _resolve sees
its own parent's in-flight loader, but ownership was keyed on
threading.get_ident() -- the parent's thread, not the worker -- so the nested
walk event.wait()ed on itself until the deadline abandoned the load (#130575).

Record in-flight load keys in a per-registry ContextVar instead. Deadline
workers start from copy_context(), so a nested walk on the parent's worker
takes the existing "recursively requested while loading" return, while
unrelated concurrent loaders (fresh contexts) still wait on the event.
2026-10-03 16:53:54 +05:30
Yuan Li
cfb005ed14 fix(plugins): avoid recursive deadline workers
(cherry picked from commit 54e2f207df63f58030d38319f37761f4fb53e97c)
2026-10-03 16:53:54 +05:30
Yuan Li
eec8928406 test(plugins): cover deadline worker re-entrancy marker
(cherry picked from commit ae9e171f142911191e4ecec99f2e48e3785f33db)
2026-10-03 16:53:54 +05:30
Yuan Li
f5d8e5e3c0 fix(plugins): propagate load re-entrancy across worker context
(cherry picked from commit 2c5d3b1140b9ab3050f7abc66a71bb84bbc08c3a)
2026-10-03 16:53:54 +05:30
kshitijk4poor
d6e740b874 fix(compression): mark a failed delegate_task stub too
Last templated summarizer that ignored the payload outcome: a delegate_task tool_error
(unknown action, spawn failure) compressed into the same stub as a successful delegation.
2026-10-03 16:53:37 +05:30
kshitijk4poor
9babfa780e test(compression): fold the outcome-stub tests into test_context_compressor
The two parametrized invariants pinned the same contract as
TestSummarizeToolResultSkillTools (stub carries " FAILED: <error>", stays
one line) in a separate file. Move them next to it as
TestSummarizeToolResultOutcome and delete the new file.

Every row now asserts the exact stub. The old substring check let the
cronjob "create" row pass with reason="" (always true).

Gate findings BN03b r1 reuse + quality (Low).
2026-10-03 16:53:37 +05:30
kshitijk4poor
a6f8720b08 refactor(compression): name the shared outcome probe for what it is, parse once
_skill_result_failure_suffix is now the outcome probe for every templated,
browser, web, vision, cron, process and unmapped-tool stub, but its name and
docstring still said "skill", which suggests skill-specific logic. Rename it
to _result_failure_suffix (all callers are in this module; no test imports it).

Split out the dict-taking _payload_failure_suffix so _sum_cronjob_manage and
_sum_process_manage parse the result once instead of twice (a process poll
can carry a large output), and build the exit-code suffix through
_failure_suffix like every other branch. No behaviour change.

Gate findings BN03b r1 quality + efficiency (Low).
2026-10-03 16:53:37 +05:30
kshitijk4poor
aa04942ad5 fix(compression): don't stub a self-killed process or a skipped cron run as FAILED
kill_process records exit_code -15 with completion_reason "killed", so a
later poll of a process the agent stopped on purpose compressed into
"[process] poll ... FAILED: exit code -15". A manual cron run the scheduler
was already firing reports execution_skipped, and was stubbed as
"FAILED: Already being fired ...", although the job may be running right
now. After compaction the agent read both as failures.

Skip the exit-code suffix when completion_reason is "killed", and mark
execution_skipped as " SKIPPED: <reason>" (_failure_suffix takes the label);
execution_error and execution_success=False still mark FAILED.

Gate findings BN03b r1 2c + quality (Low, convergent).
2026-10-03 16:53:37 +05:30
kshitijk4poor
d70dfa8726 fix(compression): mark failures in web_extract, browser, vision and unmapped-tool stubs
The outcome suffix stopped at _sum_template, so the rest of the same bug
class still compressed a failed call into a stub that reads like a
success: web_extract {"error": "403 Forbidden"}, a failed browser_* call,
a refused vision_analyze, and every unmapped tool. The unmapped fallback
covers every MCP and plugin tool, so it was the widest remaining gap.

Append the same top-level outcome suffix in _sum_web_extract, _sum_browser,
the vision_analyze summarizer and the generic fallback in
_summarize_tool_result_unguarded. Success payloads and non-JSON content
give the same stub as before.

Gate finding BN03b r1 2c (Medium).
2026-10-03 16:53:37 +05:30
kshitijk4poor
0d0bed693a test(compression): trim #131244 tests to two parametrized invariants
The salvaged file carried 17 tests across four classes. Keep two
parametrized invariants: every failing templated/cron/process call is
marked FAILED in its stub, and healthy calls (including a cron job with a
stale stored error, a clean exit and a still-running poll) stay unmarked.
The patch_tool and skill_manage cases cover other summarizers already
pinned in test_context_compressor.py.
2026-10-03 16:53:37 +05:30
Konstantin Khlopkov
7de2fbb98f fix(compression): mark a dead cron run and a non-zero exit in their stubs
A manual cronjob run reports a dead or skipped job inside the job view while the tool
call itself succeeded, so the payload is a clean {"success": true, "job": {...}} and the
top-level outcome probe finds nothing to mark. A poll of a finished process reports
exit_code and completion_reason instead of an error, so a non-zero exit is invisible the
same way. Both compressed into the byte-identical success stub, which is the failure this
series removes.

Each tool now reads its own payload: the cronjob stub looks at this call's
execution_error / execution_skipped / execution_success and at nothing else, because the
job view also carries state left by earlier runs; the process stub marks a non-zero
exit code, while a process still running reports none and stays unmarked.

(cherry picked from commit 295e23e3bfc084f20844bf518a244d41421d3f66)
2026-10-03 16:53:37 +05:30
Konstantin Khlopkov
b66dcf32e0 fix(compression): keep the outcome in templated tool-result stubs
_sum_template formatted from the tool arguments and the result's length only, so every
tool routed through it dropped its outcome. A refused read, a rate-limited search, a
rejected cronjob, an unknown process session and a tts call with no voice all compressed
into the same stub as the success they never were, and the post-compaction agent went on
to report the success (#131244).

Append the failure suffix the skill summarizers already use, so the templated family keeps
its outcome from the one helper that already knows how to read it. Stubs for calls that
report no failure are unchanged.

Complementary to #131247, which gives write_file and patch summarizers of their own.

(cherry picked from commit 6302f87ff52c10db2b43521b733914748b4bcca2)
2026-10-03 16:53:37 +05:30
kshitijk4poor
21d59f18e7 docs(tui_gateway): name both scope-release helpers in the follow-up comment 2026-10-03 16:52:17 +05:30
kshitijk4poor
7b1979dc52 test(tui_gateway): split turn_env into turn_stubs + inline Thread patch
Gate finding (quality, Low): test_turn_settles_before_post_turn_trim
repeated seven of turn_env's stubs line by line only because turn_env also
swaps threading.Thread for an inline runner and this test needs a real
worker thread. turn_stubs now holds the stubs; turn_env = turn_stubs + the
Thread patch; the settle test uses turn_stubs.
2026-10-03 16:52:17 +05:30
kshitijk4poor
544e369d85 fix(tui_gateway): run the post-turn trim even if a settle step raises
Gate finding (2c, Low): _post_turn_housekeeping ran as the last statement
of the settle block, so a raising _emit_settled_session_info (its
_session_info reads config unguarded) skipped the memory trim and left the
profile HERMES_HOME override bound on the worker thread. Wrap the settle
block in try/finally so the housekeeping always runs; the audio teardown
already happens before settlement.

The settle-before-trim test gains a row where _emit_settled_session_info
raises and still requires the trim to run.
2026-10-03 16:52:17 +05:30
kshitijk4poor
9b59ce6426 fix(tui_gateway): end turn audio before settlement, defer only the trim
Gate finding (2c + quality, convergent): the settle-before-trim split moved
the thinking-sound stop and the TTS end-of-text sentinel after
session["running"] = False. Both are non-blocking (a lock + Event.set; an
unbounded queue put), so deferring them gained nothing, and it opened a
window where a prompt admitted on the same session during the trim had its
thinking sound stopped by the old thread and the old stream's final chunk
cut as a barge-in. They run in _release_turn_scopes again, before
settlement; _post_turn_housekeeping keeps only the trim and the
HERMES_HOME override reset. (The original commit subject says "trim/TTS";
only the trim is deferred now.)

The settle-before-trim test now also asserts both audio teardown steps ran
while the session was still running.
2026-10-03 16:52:17 +05:30
StanleyStetson
5babdc36f6 fix(tui_gateway): settle the turn before best-effort post-turn trim/TTS
The prompt-turn finally ran trim_memory (gc.collect + malloc_trim, 20-50 s
on multi-GB heaps), the thinking-sound stop and the TTS sentinel BEFORE
marking the session idle and logging the "tui turn finished" bookend. A
slow or blocked trim therefore kept the session busy and the bookend
missing for its whole duration.

Split _finish_turn into _release_turn_scopes (snapshots, one-turn model
restore, approval/secret/terminal/session scopes — unchanged order and
logging) and _post_turn_housekeeping (trim, thinking sound, TTS sentinel,
then the HERMES_HOME override reset so the trim still reads the session
profile's config, #58576). The settle block runs between them verbatim.

Refs #131740 (the reported 600 s watchdog hang is inside
agent.run_conversation, so this hardening does not close it).
Salvaged from #131773.
2026-10-03 16:52:17 +05:30
kshitijk4poor
d440c5b59b refactor(state): trim the flag-import comment and MB-passing test lines
Gate finding (BN07 quality #3): the comment above _IMPORT_FLAG_COLS restated the
test module docstring (and called the sweep's auto_archived a user decision); cut
it to one WHY line. The donor-content and retry-adoption assertions exercise
adopt_session_lineage_from behaviour that already holds on main (verified: they
pass against the merge-base prod file), so they add no teeth for this fix.
2026-10-03 16:41:37 +05:30
kshitijk4poor
47b7638e6e fix(state): import auto_archived only on an archived row
Gate finding (BN07 quality #2): auto_archived was imported independently of
archived, so a payload with archived=0, auto_archived=1 stored a live row marked
as swept, a state the idle sweep never produces (it only sets the flag on rows
it archives). Clear it unless the row is archived.
2026-10-03 16:41:37 +05:30
kshitijk4poor
66999df56c fix(state): coerce imported session flags like the int columns
Gate finding (BN07 quality #1): the session flags were read by truthiness, so a
hand-edited export carrying the string "0" imported as pinned/hidden/archived = 1.
The same function already coerces message flags with _coerce_or(..., int, 0);
use it for the session flags too.
2026-10-03 16:41:37 +05:30
kshitijk4poor
0e4f836a8c test(state): fold #131161 import-protection assertions into keep-flags tests
A legacy export without flag columns must still import as an ordinary
visible session; the adopted canonical Bot Chat must keep its rename guard;
the donor must stay untouched; and a retried adoption must skip the
already-imported row instead of restoring the donor retirement archive
over the local copy. Extends the two existing tests instead of adding a
duplicate file.

Co-authored-by: itsflownium <157689911+itsflownium@users.noreply.github.com>
2026-10-03 16:41:37 +05:30
John Paul Soliva
618a4d5dbd docs(sessions): a restored export keeps its pinned, hidden and archive flags
(cherry picked from commit c04df9dbe560c1de862d020f69073c535e021995)
2026-10-03 16:41:37 +05:30
John Paul Soliva
98cb791b81 fix(state): a restored sweep archive stays the sweep's
import_sessions copied archived but not auto_archived, so a chat the
idle sweep had archived came back as a deliberate archive. Resume and
compression only un-hide sweep archives (_unarchive_auto_archived_lineage),
so after a restore the chat stayed hidden for good.

(cherry picked from commit 130aa6f3cddee06238dd46f9dbc50eb8acef552f)
2026-10-03 16:41:37 +05:30
John Paul Soliva
db6ffa8ea6 fix(state): an adopted Bot Mode chat stays hidden after import
Stranded-session adoption copies a profile bot's chat from the default store
through import_sessions, which dropped hidden. Bot Mode chats are hidden, and
hidden is what makes the canonical Bot Chat canonical: the adopted copy showed
in every session listing, lost the rename guard on its registry title and
became a stale-archive candidate. hidden joins archived and pinned as a
restored flag.

(cherry picked from commit 1deb08a9cfd08845bc1de25cc502c3bcb51e8569)
2026-10-03 16:41:37 +05:30
Drexuxux
e00b87869c fix(state): keep the pinned "keep" flag when importing sessions
import_sessions restored archived but not pinned, so a session restored from
a backup (dashboard import of `hermes sessions export`) or adopted from the
default store came back unpinned. Pinned is what the startup prune and the
stale-archive sweep exempt: an old pinned session was deleted by the next
startup after the restore.

Ported onto the split import helpers from #76206.

(cherry picked from commit 59ea00ea713f90a1aa0487e05de79e6690be0bdf)
2026-10-03 16:41:37 +05:30
kshitijk4poor
1c520f4a3d chore: map Drexuxux for salvage of #130345
The legacy AUTHOR_MAP already maps this email, but the per-email file the
release tooling now reads was missing; add it so the cherry-picked commit
is credited.
2026-10-03 16:41:37 +05:30
kshitijk4poor
eba3414727 refactor(discord): derive the resolved-ID count and accessor from the set
Quality-gate Lows (BS03 r1): resolved_count always equalled
len(self._username_resolved_ids) (each name is discarded from to_resolve on
its first match, so it adds exactly one ID), and resolved_allowlist_user_ids
filtered isdigit()/"*" out of a set that only ever holds str(member.id) and is
always initialised in __init__ and reset by the resolver. Drop the counter, the
filter, the getattr fallback (no test builds the adapter without the resolver
and then calls the accessor) and the stale "Numeric only" docstring line.
Behaviour unchanged (probe-BS03-1 output identical before and after).
2026-10-03 16:37:12 +05:30
kshitijk4poor
9a6c655d6e test(discord): pin the resolved-ID accessor through the real resolver
Quality-gate Low (BS03 r1): test_filters_usernames_and_wildcard seeded
_username_resolved_ids with "teknium" and "*", values the resolver can never
write there, so it only pinned a defensive filter against impossible state.
Fold it into the kept accessor test, which now drives the real
_resolve_allowed_usernames (wildcard, unresolved username and numeric entry
in the allowlist) and asserts only the username-resolved ID comes back. It
fails on the pre-salvage accessor, which leaked the numeric entry.

Also drop the redundant _username_resolved_ids reset in the helper (the
resolver resets it first), hoist `import asyncio` to the module imports and
restore PEP 8 two-blank-line spacing between top-level definitions.
2026-10-03 16:37:12 +05:30
kshitijk4poor
72faaf8ad8 test(discord): trim allowlist salvage tests to the two headline invariants
Keep the nickname/global-name impostor case (parametrized) and the
revoked-numeric-entry-after-env-reload case; both go red on the pre-fix
adapter. The display-name hint and numeric-only snapshot tests cover
the same code paths as the kept ones.
2026-10-03 16:37:12 +05:30
kshitijk4poor
d13d92cc82 fix(discord): reset username-resolved IDs before the empty-allowlist guard
_resolve_allowed_usernames reset _username_resolved_ids only after the
early return for an empty allowlist / missing client, so a reconnect
after the allowlist was emptied kept the previous connect's resolved IDs
in the set the gateway authz union reads. Reset first so the set always
reflects the current connect.
2026-10-03 16:37:12 +05:30
EloquentBrush0x
462389ea5e fix(discord): a numeric allowlist entry removed after connect stops authorizing
resolved_allowlist_user_ids() returned every numeric ID in the adapter's
connect-time _allowed_user_ids, and the gateway authz layer unions that set
into the reloaded DISCORD_ALLOWED_USERS. So an ID removed from the allowlist
after connect (hermes pairing revoke from the CLI or dashboard, or a hand
edit) kept authorizing until the gateway restarted. #130920's in-place purge
only reaches adapters in the same process, and those commands run outside it.

Track the IDs _resolve_allowed_usernames actually resolved from username
entries and return only those: they are what the per-turn .env reload loses
(a0a63a1bc2); numeric entries are already read live from the env.

(cherry picked from commit 90451505545745dbb27ecad28f2c431ad5afd301)
2026-10-03 16:37:12 +05:30
EloquentBrush0x
675bad762a fix(discord): resolve allowlisted usernames by username only, not display name or nickname
_resolve_allowed_usernames resolved a DISCORD_ALLOWED_USERS name through member.name,
member.display_name or member.global_name, and the first match took the slot. Any member of a
shared server could set a nickname or display name to an allowlisted username, get the
resolved ID (DMs, tools, approval buttons) and lock the real user out.

Match the unique account username only, as #125034 did for SimpleX display names. An entry
that matches only a display name stays unresolved and gets a connect-time hint. Tests cover
a copied nickname and a copied display name seen before the real user, and the hint; the
Discord docs now say display names and nicknames are never matched.

(cherry picked from commit 252f0bccab39afe94f0f71254e47a8f785ec1fb0)
2026-10-03 16:37:12 +05:30
kshitijk4poor
0089fca37d fix(slack): gated slash commands get a bare ack, not "Running /x"
The channel gate ran inside _handle_slash_command, after the bolt listener had
already acked with an ephemeral "Running /x". In an ignored or non-allowed
channel the invoker was promised a run that never came, and an ignored channel
("never touched") still got a bot reply. Move both checks into
_slash_channel_gated() and consult it before the ack: gated -> bare ack().
_handle_slash_command keeps calling the same predicate for direct callers.
The listener becomes the _handle_hermes_command method so the existing
parametrized gate test drives the real ack path (BN11 gate r1 2c Low finding).
2026-10-03 16:36:48 +05:30
kshitijk4poor
46dd3161bb refactor(slack): classify the 1:1 DM once in _handle_slash_command
The allowed_channels guard recomputed str(channel_id).startswith("D") inline and
the next line computed it again as is_dm. Hoist is_dm above the gates and reuse it.
Same behaviour, one DM classification (BN11 gate r1 quality/reuse Low finding).
2026-10-03 16:36:48 +05:30
John Paul Soliva
b376591778 fix(slack): slash commands obey allowed_channels and ignored_channels, like messages
The message path drops a message from an ignored channel (_prefilter_inbound)
and, when allowed_channels is set, from any non-1:1 conversation outside it
(_channel_gate_allows). _handle_slash_command applied neither, so a slash
command ran in a channel the operator had excluded. Apply both gates by
channel id alone, before any lookup, so a failed conversations.info can never
drop a control command.

Rebuilt from 2b7cde506c (#128767). Fixes #130358.
2026-10-03 16:36:48 +05:30
kshitijk4poor
c3dffacdf3 test(sessions): drop the explicit archived-id export check
Gate finding (BN06 r1 quality, Low): the --session-id path never reads the
export filters, so this block passes on the merge base too (probe-BN06-3) and
pins behaviour the stack does not change.
2026-10-03 16:36:00 +05:30
kshitijk4poor
0ed89ea329 test(sessions): assert the exact backup set instead of echoing prune's query
Gate finding (BN06 r1 quality, Low): the subset check rebuilt candidates with
the same list_prune_candidates kwargs _cmd_export hard-codes, so it would still
pass if prod and test both dropped include_pinned. Assert the exported ids
directly; keep the pinned/archived flag asserts.
2026-10-03 16:36:00 +05:30
kshitijk4poor
2dbe76a978 refactor(sessions): check an explicit export --session-id once in _cmd_export
Gate finding (BN06 r1 quality, Low): the missing-id check lived in three
places (_collect_sessions, _export_trace, _export_markdown_single), and
_collect_sessions had grown a list|int|None return with an isinstance
branch in _export_flat just to carry the exit code. Resolve the id once at
the top of _cmd_export and return _not_found's non-zero code there; the
per-format copies and the int return are now dead and reverted.
2026-10-03 16:36:00 +05:30
kshitijk4poor
814c7de76e docs(sessions): say filtered export covers pinned/archived in --help
The salvaged #131075 fix makes filtered `hermes sessions export` include
pinned rows (archived were already included), so a backup taken before
`prune --include-pinned` holds every row prune can delete. Surface that in
the export --older-than help so users do not assume prune's keep rules
also narrow the backup.

Co-authored-by: Alisson Galindo <alissongalindo@users.noreply.github.com>
Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-03 16:36:00 +05:30
itsflownium
5b33f3128a fix(sessions): include protected rows in retention backups
(cherry picked from commit 891575265e657acf5d0b5e6179d11013407a7f8c)
2026-10-03 16:36:00 +05:30
kshitijk4poor
53dacc4e51 test(auxiliary): tighten the Timer-never-closes-a-real-SDK-stream asserts
The stranger[0] membership assert was circular (it re-found an element
taken from the same list) and raised IndexError instead of a useful
message when the Timer did nothing. Assert the list is non-empty and
shutdown-only in one line. The trailing time.sleep(0.2) did no work:
every stranger event is recorded before woken.set(), and the owner's
close events happen before pytest.raises exits.
2026-10-03 16:35:32 +05:30
kshitijk4poor
efa901bee7 refactor(auxiliary): resolve the attempt stream's socket through _socket_from_stream
_attempt_stream_socket re-implemented a narrow slice of the existing
_socket_from_stream helper (get_extra_info only) behind a blanket
try/except. Delegating to the shared helper keeps one socket-discovery
path (it already suppresses get_extra_info failures and covers the
_sock / anyio backends), and the remaining lookup cannot raise.
Also trims the close_attempt_stream docstring to the essential why.
2026-10-03 16:35:32 +05:30
kshitijk4poor
8ec2559e2b refactor(auxiliary): drop the redundant guard around _shutdown_socket in close_attempt_stream
_shutdown_socket already swallows OSError itself (agent/agent_runtime_helpers.py:
"except OSError: pass"), so the extra try/except in close_attempt_stream only hid
programming errors behind a debug log.

Closes #130115.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-03 16:35:32 +05:30
Justas Cesnauskas
b1aa0a9be0 fix(auxiliary): the watchdog Timer shuts the attempt stream's socket down instead of closing the stream
_CodexStreamGuard._close_client_on_timeout keeps the FD-ownership contract for the client's pool sockets
(shutdown() from the Timer, close() only on the owning thread), but then called close_attempt_stream(),
and an SDK Stream.close() on an unfinished response ends in sock.close() (httpx Response.close ->
httpcore HTTP11Connection.close -> SyncStream.close). That releases the descriptor while the owner is
still inside SSL_read: the kernel hands the number to the next open() and OpenSSL writes a TLS alert
record into that file. Two SQLite files in a gateway process lost bytes 5..28 of their header this way
(b"SQLit" + 17 03 03 00 13 ...), 15 ms after "Codex auxiliary client aborted (timeout,
tcp_force_closed=1, deferred_close=stranger_thread)".

close_attempt_stream() now does a real close() only on the owning thread. From any other thread it
shuts down the socket under the stream (stream.response.extensions["network_stream"]), which wakes the
reader and keeps the FD; the owner closes the stream in its finally (release_stream). A stream with no
reachable socket (a test double, a non-httpx transport) is closed as before.

Fixes #130115. Follow-up to #70773.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 33cfb5ea6550f4a56ea0faa072ddf7f91412af49)
2026-10-03 16:35:32 +05:30
kshitijk4poor
5b124faec4 chore: map justascesnauskas for salvage of #130176 2026-10-03 16:35:32 +05:30
kshitijk4poor
57ba305a38 test(desktop): describe the interim-redelivery specs as they now stand
The header said the duplicate paints "on current main" (stale once the
fix lands) and pointed at boundary "specs below" when only the
message.start spec remains. Also apply prettier to the delta helper,
which --check flagged.
2026-10-03 16:34:20 +05:30