Skip to content

fix(*): settle the silent-turn findings left open when #1092 merged - #1146

Open
LivXue wants to merge 13 commits into
mainfrom
fix/silent_turn_review_followups
Open

LivXue wants to merge 13 commits into
mainfrom
fix/silent_turn_review_followups

Conversation

@LivXue

@LivXue LivXue commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #1092, which merged before its last review round was
answered. This settles the seven non-blocking findings of that round's
delta acceptance on the merged head, the Windows note from the Windows
review, and the five non-blocking findings of this PR's own acceptance
review, one commit per finding:

  • The kernel reads no_output off the instance. A runner's
    AnswerlessTurnError subclass that skips the base __init__, or
    guards no_output with a property that raises, made the scheduler's
    failure branch raise itself, so no TurnFailed went out.
    no_output_of reads the instance dictionary the way
    full_detail_of already does.
  • Only a proactive turn taught the marker reads the loose rule. The
    cron reminder and a sub-agent's unprompted report teach
    SILENCE_INSTRUCTION; a heartbeat task, a relayed sub-agent result
    and a sentinel notice never do, yet swallowed a reply that was, or
    ended on, No reply or Silent. The rule is now picked by the
    origin and by whether the prompt the model read carries the
    instruction (teaches_silence). Every other turn honours only the
    bracketed [SILENT] and streams as a person's turn does.
  • A held note's marker continuation is read as one reply. On a
    proactive turn nothing of a cut partial was shown, so a continuation
    that is the marker alone is read with what came before it, on its own
    line, as the uncut reply would be, instead of leaving the note to be
    delivered.
  • A continuation that raises ends the turn on what streamed, with
    PARTIAL_STREAM_NOTE, as one that answers with an error already did.
    A call that raises with no cut behind it still fails the turn. It
    ends there once the stream's own retries are spent: its reconnects
    and, on a failure it reads as transient, llm_error_retry_delays
    (15, 30 and 60 seconds by default), which CONTEXT.md now spells
    out.
  • A turn whose reply the reader saw is not taken back. The message
    tool's send callback never sees a target: it hands every message to
    the turn's own reader. So sent_in_turn now reports any send that
    went out, and a turn whose tools delivered files is stamped
    replied_via_tool too. The session list and the ACP replay keep both.
    Live, a turn that delivered files and then ended on the marker is no
    Silent Turn either: no silent_turn notice, TurnOutcome.silent
    false and explicit_reply true, so cron counts it toward
    silent_fire_limit, records it for the sentinel, and does not hand
    it to heartbeat as a run that said nothing.
  • Which turns are taken back is one rule, TakeBack in
    session/turn_shape.py, read by the session list, the ACP replay and
    the live save; a test feeds the same turns to all three and holds
    their answers equal. A turn it takes back files no
    unanswered-question notice, so no orphan question row is left behind
    it, and a person's silent turn still files its notice. Sharing the
    rule ends one disagreement: the replay closed a turn at its marker
    and the list did not, so a row between that marker and a second one
    was drawn by the replay and hidden from the list. Both now keep it.
  • A silent turn takes back only what its closing reply showed. The
    silent_turn notice's detail now carries the text the reply read
    as the marker had already streamed (a think block it inlined ahead of
    it), counted over the step the reader draws it in. The web page and
    the TUI strip exactly that suffix and keep the narration before it;
    the web page used to cut at the step's last tool row, which a
    message-only call does not draw, and the TUI cleared its whole stream
    buffer.
  • Windows: a transcript saved with a BOM or in a code page. The
    session list, the loader and _stored_key read utf-8-sig, so a BOM
    transcript is listed, opens with its title and archive flag, and
    claims its key. The list leaves an undecodable file out instead of
    failing; a save or a metadata patch onto such a file still raises and
    writes nothing, rather than appending UTF-8 lines no reader could
    open. The ACP replay tests build their expected paths under
    tmp_path.

Existing tests whose meaning changed, each with its reason in the diff:
the loose-rule mechanics tests now run a taught turn; the TUI and web
take-back tests now send the detail the server would send; a send to
another chat is now an answer; a refused continuation now ends on its
words (the broken-turn test was split into its cancel and refusal
cases); the code-page listing test now also asks
find_most_recent_chat_id.

Not changed:

  • The message tool still tells the model "Message sent to
    {channel}:{chat_id}" for a send whose callback ignores the target.
    That predates feat(*): align empty-reply recovery with Hermes and let a turn end in silence #1092 and is outside these findings.
  • A runtime-opened turn that is taken back files no
    unanswered-question notice, so the live web page and TUI, which draw
    that turn until a reload, and the next turn's model do not get the
    question either. Filing it and dropping it with the turn in the two
    stored readers would keep it for them; that was left out.
  • Three readers of the sessions directory outside the session store,
    the sentinel's daily_analysis.py and currently_focused.py and the
    behaviors extractor, still open a transcript as plain UTF-8: a BOM
    costs them the metadata key, which they then take from the path, and
    a code-page file raises out of the read. That predates feat(*): align empty-reply recovery with Hermes and let a turn end in silence #1092 and is
    outside these findings.

Interplay with the open #1099, which routes a send naming another chat
to that chat: with it, such a send returns before the callback, so it
still does not count as the turn's answer, and the two compose. They
touch the same lines of MessageTool.execute, and whichever lands
second also drops "whatever chat the call names" from the
sent_in_turn docstring and the CONTEXT.md entry.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

On a 32-core Linux box, at the head commit rebased onto main at
7c12b93fe unless a line says otherwise:

  • uv run --frozen --all-extras pytest -q -n 4: 37195 passed, 8
    failed, the same 8 as before the rebase, all local to this box and
    none in a module this diff changes: five
    test_config_update_providers.py tests read the box proxy (the file
    passes 200/200 with the proxy variables unset),
    test_install_script.py finds an npm on PATH,
    test_subagent_node_runtime.py runs as root, and
    test_knowledge_parser_excel.py gets no .xls from the local
    LibreOffice.

  • The 12 changed Python test files under the CI coverage flags with
    --idle-ceiling-strict, serially: 1551 passed, no idle-ceiling hit;
    every executable line the last five commits add is covered.

  • Each commit with the tests its files reach. The first eight, alone,
    before the rebase onto 3ba6179fb (which git range-diff shows
    replayed every commit unchanged, as it does onto 7c12b93fe): 54,
    199, 504, 475, 477, 1172, 974 passed, and the eighth was the head
    then. The last five, each on top of the ones before it: 426, 594,
    863, 4 and 907 passed.

  • ui-web: npx vitest run 4520 passed (262 files);
    npx vitest run --no-file-parallelism scripts/gates 257 passed;
    npm run type-check and eslint on the changed files clean.

  • ui-tui: npm test 2275 passed, 2 failed in modelPicker.test.tsx
    and newInstancePicker.test.tsx, both 38/38 when rerun alone
    (full-run load); npm run type-check and eslint on the changed
    files clean.

  • node scripts/gen-rpc-client.mjs --check (ui-web) and
    node scripts/gen-rpc-types.mjs --check (ui-tui): in sync.

  • make lint-types clean; make lint-imports 10 kept; ruff check and
    format clean.

  • scripts/check_commit_messages.py origin/main..HEAD,
    scripts/check_large_files.py and scripts/check_source_language.py
    over origin/main...HEAD: clean (the last beside a control that adds
    one CJK comment to a .py file and fails).

  • Every fix was watched failing before its change, and each new guard
    was mutated after it: 33 mutants for the first eight commits across
    the loop, the web page and the TUI, and 16 for the last five, each
    killed by at least one test.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered

  • Backward compatibility considered

  • Rollback path is clear for risky changes

  • A heartbeat task, a relayed sub-agent result or a sentinel notice that
    answers No reply. now reaches the reader, and its stream is no
    longer held until the call ends. Only an exact [SILENT] keeps it
    silent.

  • A message-tool send that names another chat now counts as the turn's
    answer; it always reached the turn's own reader.

  • The silent_turn notice's detail was always empty and now carries
    text. The bundled web page and TUI do not draw it; a third-party
    client that draws every notice's detail under its label would show
    the think block there.

  • teaches_silence reads the whole prompt, so a relayed result that
    quotes SILENCE_INSTRUCTION makes that turn read the loose rule. That
    can only withhold a reply the model shaped as a marker, which text in
    the prompt could already ask the model for.

  • A cron run that delivers files and then ends on [SILENT] now
    counts toward silent_fire_limit and is recorded for the sentinel,
    as one that sent a message already was.

  • A save or a metadata patch onto a transcript saved in a Windows code
    page raises, as it does on main, rather than losing the turn.

  • Each finding is its own commit, so any one can be reverted alone.

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the full github/main...HEAD diff and found no plain error worth raising. The eight follow-ups preserve one coherent silence protocol: the rule is selected from the prompt the model actually reads, cut-answer continuations retain already-streamed work, tool-delivered replies survive live and replay views, and clients retract only the exact suffix named by the runtime. I specifically tried to refute the new behavior against continuation exceptions, silent wrap-ups, failed message sends, file deliveries, runtime/user turn boundaries, partial client joins, and persisted session/ACP replay.

Coverage included AGENTS.md and the Runtime/TUI/Web context contracts, the diff, affected callers, the #1092 review history and this branch's commit history, backward compatibility of the RPC notice and transcript shapes, test changes for weakening, and the kernel/layer architecture. git merge-tree --write-tree HEAD github/main is clean and GitHub reports the revision mergeable.

Verification at 3f5805d94446: 1,523 changed Python tests passed; focused web tests passed 283/283 and focused TUI tests passed 56/56. Python Ruff and targeted type checks, both UI type checks, generated RPC checks, changed-file ESLint, all 10 import contracts, commit-message/source-language/large-file gates, and git diff --check passed. A plain Python environment first failed setup because the EverOS dev plugin was absent; rerunning with the repository's dev extra produced the 1,523 passing result. An attempted --all-extras environment could not build the unrelated optional datrie segmenter because this host has no gcc, so no all-extras test result is claimed.

@LivXue

LivXue commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review — PR #1146 @ 3f5805d944460e5d0ca41b3484c309f3050cf2bc

Scope: the full PR diff at this head against base 3ba6179f (39 files, +1071/−250): the silent-turn follow-ups left open when #1092 merged — which turns read the loose silence rule (now keyed on the prompt actually teaching SILENCE_INSTRUCTION), what a silent reply streamed before it was read as the marker (LoopOutcome.shown, the silent_turn notice detail), how a tool-sent reply or a file delivery marks replied_via_tool, and how the session list reads a transcript's metadata. Reviewed for Windows behaviour and run on native Windows.

Result: the one directly Windows-facing change in this diff is correct in direction.

  • raven/session/manager.py:911 now opens a transcript with utf-8-sig, so a file a Windows editor saved as UTF-8 with a byte order mark no longer puts the mark in front of the metadata line and drops the session from the list.
  • raven/session/manager.py:944 catches UnicodeDecodeError together with OSError, so a transcript saved in a Windows ANSI code page is skipped the way an unreadable one already was, instead of its decode error failing the whole listing.
  • Boundary worth knowing, not a defect: a UTF-16 transcript (for example one written by PowerShell 5.1's > redirection) still fails to decode under utf-8-sig and is therefore listed as unreadable rather than decoded. That is the intended "leave it out" behaviour the commit describes.
  • The rest of the diff touches no path, subprocess, signal, socket, permission, or encoding logic beyond the above (searched the diff for those constructs); the silence/take-back changes are pure in-memory state.

Verification on native Windows 11, detached worktree D:\tmp\rv-1146 at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • uv run --no-sync pytest tests/test_acp_replay.py tests/test_acp_updates.py tests/test_agent_context_silence.py tests/test_agent_loop_broken_turn.py tests/test_agent_loop_empty_recovery.py tests/test_agent_loop_run_emit.py tests/test_core_cron_stack.py tests/test_rpc_spine.py tests/test_session_manager.py tests/test_session_turn_shape.py tests/test_spine_scheduler.py tests/test_subagent_manager.py -q → 26 failed, 1497 passed
  • the same twelve files at base 3ba6179f → 28 failed, 1409 passed
  • no failing ID appears only at the head; two base-only test_acp_replay.py::TestToolCalls failures do not reproduce at the head. The remaining failures are the known inherited Windows categories. The new session-manager tests, including the BOM and code-page cases, pass.

Coverage gaps: the ui-tui/ui-web test files in this PR were not run — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries), so vitest cannot run; that side is covered by reading only. The full suite was not rerun.

@0xKT

0xKT commented Oct 29, 2026

Copy link
Copy Markdown
Member

Not a blocker - acceptance on 3f5805d9 merged onto fb0dd515. The seven #1092 follow-ups land as described:

Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

  • no_output is read off the instance dict;
  • untaught proactive turns honour only [SILENT];
  • a held note's marker continuation reads as one reply;
  • a continuation that raises ends on the partial;
  • a send to another chat counts as an answer;
  • the take-back is exactly the shown suffix on web and TUI;
  • BOM transcripts are listed.

What is left, all non-blocking:

  1. Code-page transcripts: a loud failure became a silent one on the save path. _scan_file now returns an empty reading on UnicodeDecodeError (raven/session/manager.py:944-947) to keep one file out of the list. But _record_written_since (:1170) and append_metadata_patch read through the same function. So a turn on a transcript saved in a code page now appends UTF-8 lines to a file no reader can open: load returns nothing, the session never lists, and nothing reports it. On base the save raised and wrote nothing. Catching the decode error only in the list's caller would keep both behaviours.
  2. BOM tolerance reaches the list only. _scan_file reads utf-8-sig, but read_session_file (:536) and _stored_key (:78) still read plain utf-8. A BOM transcript whose only metadata record sits behind the mark is listed with its title and archive flag, opens without them, and the first save drops them.
  3. File delivery is "answered" on disk but "silent" live. _stamp_replied_via_tool now also fires on _delivered_files (turn_path.py:3517), but the live verdict (sent_by_tool, :3512) still drives the SILENT_TURN notice and TurnOutcome.silent. A deliver_files then [SILENT] turn is a Silent Turn on the wire and answered in the list / ACP replay, which contradicts rpc/models.py:4015-4021.
  4. A continuation that raises still waits out the stream's ladder (15+30+60 s, with no LLM_RETRY notice) before ending on the partial. The new CONTEXT.md text (:910-912) says "with no wait". The test can't see it because conftest shortens the ladder.
  5. The take-back rule now has a third decider. _taken_back_whole (turn_path.py:479-498) re-derives, over live rows, the rule session/manager.py and acp/replay.py apply to stored rows, and gates _file_unanswered on it. They agree on every shape production produces today; a shared predicate in session/turn_shape.py would keep it that way. Skipping the notice also removes it for the web page, the TUI and the next turn's model, which all still draw or read a runtime-opened turn. Dropping it with the turn in the two take-back readers would avoid that.

Tests here on the merged tree: the changed Python test files via the board runner gave 1523 passed, and tests/test_kernel_budget.py 5 passed. ui-web: 3 changed test files, 283 passed. ui-tui: 2 changed test files, 56 passed.

LivXue added 13 commits October 30, 2026 08:57
The scheduler read no_output with plain attribute access while filing
a turn's failure, right below full_detail_of, which reads the instance
dictionary because a foreign getter can throw. A runner's
AnswerlessTurnError subclass that skipped the base __init__, or guarded
no_output with a getter that throws, made the failure branch raise
itself: no TurnFailed went out and the lane's worker died.

no_output_of reads it the way full_detail_of reads its field, and
anything but True means no. A test files a failure for both subclass
shapes.
The session list read every transcript as plain UTF-8. A file a
Windows editor saved with a byte order mark put the mark in front of
the metadata line, which then failed to parse and took the session off
the list; one saved in a Windows code page raised UnicodeDecodeError
out of the scan and failed the listing as a whole. The scan now reads
utf-8-sig, and leaves an undecodable transcript out as it already
leaves out an unreadable one.

The ACP replay tests resolve their working directory under tmp_path,
so the paths they expect are the ones the replay builds on Windows
too.
The session list and the ACP replay take back whole a turn the runtime
opened and the model closed with a Silence Marker the message tool did
not answer. A question that turn could not put to anyone was still
filed as a notice after the marker, and it outlived the take-back: an
orphan question row in the replay, and a session list moved by a turn
it otherwise hides. Such a turn now files none, by the rule those two
readers apply: the last user row before the marker carries an origin.
A person's silent turn, which both keep, still files its notice.
A cut answer's continuation can fail by raising rather than by
answering with an error: LiteLLMProvider.chat_stream raises its HTTP
errors once the stream's own reconnects are spent. The loop let that
escape and failed the whole turn, though the reader already held the
answer so far. It now ends the turn on the partial with
PARTIAL_STREAM_NOTE, as interrupted, as it does for a continuation
that answered with an error. A call that raises with no cut behind it
still fails the turn, and a cancel still propagates.
On a proactive turn the silence gate holds the whole call, so a cut
partial was never shown. A continuation that was the marker alone was
still dropped as one that adds nothing, which left the held note to be
delivered as the turn's answer. That drop rests on the reader already
holding what streamed, which is true only of a turn that showed it.
Where it did not, the marker is read with what came before it, on its
own line, as the same reply uncut would be.
MessageTool counted a send as answering the turn only when it named
the turn's own channel and chat. The send callback never sees a
target, though: it hands every message to the turn's own reader. A
send that named another chat reached the reader and was still read as
no answer, and a silent runtime turn whose reply was a file delivery
looked unanswered too, so the session list and the ACP replay took
back turns whose reply the reader had seen.

sent_in_turn now reports any send that went out, and a turn whose
tools delivered files is stamped replied_via_tool as well. A send that
failed still answers nobody. The wire description of replied_via_tool
says so, and the generated clients are regenerated.
Every proactive turn read the loose marker rule, but only the cron
reminder and a sub-agent's unprompted report teach the model the
marker. A heartbeat task, a relayed sub-agent result and a sentinel
notice never name it, so a reply that was, or ended on, "No reply" or
"Silent" was swallowed there.

The rule is now picked by the origin and by whether the prompt the
model read carries SILENCE_INSTRUCTION (teaches_silence). A taught
proactive turn reads the loose rule and holds its whole call; every
other turn honours only the bracketed [SILENT] and streams as a
person's does. The lesson is read off the prompt rather than a flag
beside it, so an entry that teaches the marker cannot forget to say
so.
A silent turn's notice told a client to take back "the reply", and the
web page found it at the step's last tool row. A call whose only tool
was a message that went nowhere draws no tool row, so an exhausted
turn's wrap-up erased the step's narration with it. The TUI cleared
its whole stream buffer, so narration that no tool call had flushed
went too.

The loop now counts what the silence gates put in front of the reader
in the step drawn now -- one call and its cut answer's continuations,
or the wrap-up alone -- and the SILENT_TURN notice carries it as its
detail (LoopOutcome.shown). The web page and the TUI take back exactly
that suffix of the step's text and keep everything before it, and the
line they draw no longer shows the detail. ACP writes nothing for the
notice, as before.
Catching UnicodeDecodeError inside _scan_file kept a transcript saved
in a Windows code page out of the session list, but the save path and
append_metadata_patch read through the same scan. A turn on such a
transcript then took the file for an empty one: the save appended
UTF-8 lines no reader could open, so the turn was lost and nothing
said so, and append_metadata_patch reported nothing to patch. Before
the catch the save raised and wrote nothing.

The catch moves to the two readers that only list, the session list
and find_most_recent_chat_id, through _scan_listed. A save or a
metadata patch onto such a file raises again and leaves it as it was.
The session list reads a transcript as utf-8-sig, but the loader
(read_session_file) and _stored_key still read plain UTF-8. A file a
Windows editor saved with a byte order mark was listed with its title
and archive flag, opened without them because its metadata line did
not parse, and the first save then wrote over both. _stored_key found
no key in it, so session_path would hand the file to a session of any
channel with the same chat id.

Both now read utf-8-sig, as the list does. A file with no mark reads
as before.
A turn whose tools delivered files is stamped replied_via_tool, so
the session list and the ACP replay keep it as answered. Live, a turn
that delivered files and then ended on the silence marker was still a
Silent Turn: a SILENT_TURN notice went out and TurnOutcome.silent was
true. Cron therefore left it out of silent_fire_limit and the
sentinel ledger, as if it had sent the user nothing.

Such a turn is now answered live as well: no SILENT_TURN notice,
TurnOutcome.silent false, and explicit_reply true, which _run_turn
learns from the silence sink ("delivered_files"), so cron counts it
and does not hand it to heartbeat as a run that said nothing. The
files already went out with the call that delivered them, so a turn
that delivers and then answers in words still sends the words.
CONTEXT.md put a continuation that raises in the same sentence as one
that answers with an error, which ends the turn "with no wait". A
raising continuation ends on the partial too, but only after
stream_llm_call has spent its reconnects and, on a failure it still
reads as transient, waited out llm_error_retry_delays (15, 30 and 60
seconds by default), with no retry notice shown. The entry now says
so in a sentence of its own.
Three readers decided which turns are taken back whole: the session
list over stored rows, the ACP replay over wire rows, and the live
turn, which skips the unanswered-question notice after a marker that
both stored readers drop. Each re-derived the rule, and they already
disagreed on one shape: the replay closed a turn at its marker, while
the session list and the live turn kept it open, so a second marker
after a row in between took that row back from the list but not from
the replay.

The rule is now TakeBack in session/turn_shape.py, which reads a
transcript row by row and says where a turn that can be taken back
opens and where one is taken back. All three readers use it; the
live turn hands it the rows as they will be stored, without the
recovery nudges that never are. A marker closes its turn for all
three. A test feeds the same turns to the three readers, with and
without a nudge and with the opening row filed by an earlier write,
and holds their answers equal.
@LivXue
LivXue force-pushed the fix/silent_turn_review_followups branch from 3f5805d to 755e2e8 Compare October 30, 2026 09:21
@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

@0xKT thanks for the acceptance pass. All five are addressed on this
PR, one commit each, rebased onto main at 7c12b93fe. Every fix was
watched failing before its change, and each new guard was mutated after
it.

  1. Code-page transcripts on the save path - 6b2efabf5. Reproduced:
    the save grew a cp936 file from 481 to 964 bytes of lines it still
    could not decode, while append_metadata_patch returned False
    rather than appending. The catch now lives only in the two readers
    that list (list_sessions, find_most_recent_chat_id, through
    _scan_listed); save and append_metadata_patch raise again and
    leave the file byte for byte as it was.
  2. BOM tolerance beyond the list - ecadc3bc6. read_session_file and
    _stored_key read utf-8-sig too. Reproduced as you describe: the
    session opened with metadata == {}, and the first save wrote the
    auto title and dropped archived. _stored_key also returned None,
    so session_path's glob fallback would hand the file to a session of
    any channel with the same chat id; that is fixed with it.
  3. File delivery answered on disk, silent live - 25aa46e88. A turn that
    delivered files and ends on the marker is now answered live as well:
    no SILENT_TURN, TurnOutcome.silent false. One more consequence
    than the notice: explicit_reply was false for it too, and cron
    hands a run with neither a reply nor a marker to heartbeat, so
    _run_turn now counts the delivery as an explicit reply (the
    silence sink carries delivered_files). Cron therefore counts such
    a run toward silent_fire_limit and records it for the sentinel. A
    turn that delivers and then answers in words still sends the words.
  4. The wait before a raising continuation ends - 45356cfad. The wait is
    real, and it is the stream's own: stream_llm_call spends its
    reconnects, then waits out llm_error_retry_delays on a failure it
    still reads as transient, before it raises. The CONTEXT.md sentence
    now says so on its own, apart from the error answer that ends with
    no wait. And you are right about the test: its RuntimeError reads
    as unknown, retryable, so it logs one reconnect and all three
    waits, each the 1 ms conftest leaves of the ladder.
  5. The third decider - 755e2e868. The rule is now TakeBack in
    session/turn_shape.py, and the session list, the ACP replay and
    the live save all read it; a test feeds the same turns to the three,
    with and without a recovery nudge and with the opening row filed by
    an earlier write, and holds their answers equal. Writing it found
    one shape where they already disagreed: the replay closed a turn at
    its marker and the list did not, so a row between that marker and a
    second one was drawn by the replay and hidden from the list
    (measured before this commit: the list's clock fell back to the
    turn before). A marker now closes its turn for all three. The notice
    stays skipped for a turn that is taken back, and the description now
    says what that costs the live page, the TUI and the next turn's
    model.

Verification on the new head: the full Python suite 37195 passed, with
the same 8 failures local to this box as before; the changed Python
test files 1551 passed under coverage with the strict idle ceiling,
every line the five commits add covered; ui-web's and ui-tui's changed
test files 283 and 56 passed; 16 mutants of the five fixes, each
killed. The description is updated to match.

Could you take another look at the new head, and approve it if it
holds up?

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the new revision after its rebase. git range-diff shows the eight previously assessed commits replayed unchanged; I reviewed the five added commits and tried to refute each fix against its callers and stored/live shapes. The code-page catch is now confined to list-only readers, BOM handling is consistent across metadata lookup and session loading, file delivery produces the same answered verdict live and on disk, the continuation-wait documentation matches the provider ladder, and TakeBack gives the live save, session list, and ACP replay one closing rule. I found no concrete regression.

Coverage included the repository rules and Runtime context, the delta and affected callers, rebase/commit history, backward compatibility of transcript and turn outcomes, test changes for weakening, and the layer architecture. Verification at 755e2e868010: 774 focused session/replay/agent-loop tests passed; Ruff, formatting, targeted Python types, all 10 import contracts, commit-message/source-language/large-file gates, git diff --check, and merge-tree passed. GitHub reports the current head mergeable. No review threads from my account need resolution.

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1146 @ 755e2e86801075575a9cebff9f49a57e451c9d5d

Scope: the branch is rebased onto current main (which carries #1141); the delta since the previously reviewed head 3f5805d9 includes two new transcript fixes that are directly Windows-facing, plus the non-OS changes (a turn that delivered files counting as answered live, one shared take-back rule, docs):

  • read_session_file and _stored_key now read with utf-8-sig, as the session list already did. A transcript a Windows editor saved with a byte order mark now opens with its title and archive flag, and its stored key is found — previously the metadata line failed to parse and the first save wrote over it.
  • _scan_file no longer swallows UnicodeDecodeError. Only the two readers that only list — the session list and find_most_recent_chat_id — go through _scan_listed, which leaves a code-page transcript out of the listing; a save or append_metadata_patch on such a file raises again and leaves it untouched, instead of taking it for an empty transcript and appending UTF-8 lines no reader can open.

Windows angle: the pair is right for this platform. BOM handling is now uniform (every read path uses utf-8-sig), and a Windows code-page transcript fails a write loudly rather than being silently corrupted. Appending preserves an existing BOM, since the append path never rewrites the file's start.

Verification on native Windows 11, detached worktree D:\tmp\rv-1146b at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • uv run --no-sync pytest tests/test_acp_replay.py tests/test_acp_updates.py tests/test_agent_context_silence.py tests/test_agent_loop_broken_turn.py tests/test_agent_loop_empty_recovery.py tests/test_agent_loop_run_emit.py tests/test_core_cron_stack.py tests/test_rpc_spine.py tests/test_session_manager.py tests/test_session_turn_shape.py tests/test_spine_scheduler.py tests/test_subagent_manager.py -q → 26 failed, 1525 passed
  • the same twelve files at base 7c12b93f → 28 failed, 1409 passed
  • no failing ID appears only at the head; two base-only test_acp_replay.py::TestToolCalls cases now pass. The added BOM, code-page and silence tests all pass. The inherited failures are the known Windows cases (ACP listing etc.).

Coverage gaps: the full suite was not rerun, and this PR touches no UI.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants