Skip to content

feat(agent): per-conversation reasoning effort (picker row, /effort, session.set_effort) - #1113

Open
Handsome-wzw wants to merge 9 commits into
mainfrom
feat/session_reasoning_effort
Open

Handsome-wzw wants to merge 9 commits into
mainfrom
feat/session_reasoning_effort

Conversation

@Handsome-wzw

@Handsome-wzw Handsome-wzw commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

One conversation can now ask for more or less thinking than the configured default
without moving that default:

  • The WebUI's model picker gains a Reasoning effort row under the sub-agent tier
    row: Default / Minimal / Low / Medium / High. The help mark says what Default means
    on this install (the global setting, or the provider's own default when none is set).
  • The TUI gains /effort [minimal|low|medium|high|default]; /effort alone reports,
    and the status bar follows the pick.
  • New RPC session.set_effort (report, set or clear, told apart by which fields are
    present, the shape session.set_mode has). Every answer carries default and
    options, so one round trip draws the control. session.info carries
    reasoning_effort for the status bar.
  • The pick is kept on the session's record (raven/agent/session_effort.py, metadata
    key reasoning_effort, null once cleared), so it outlives a gateway restart and
    comes back on resume. The turn entrance reads it beside the model and hands it to
    _run_agent_loop, which ranks it: configured default < the mode's effort < this
    pick < a hook's rollback override. Raven's own calls only; sub-agents keep the
    configured default. Lands on the next turn.
  • The words are REASONING_EFFORTS in config/schema.py, shared with the console's
    checker for agents.defaults.reasoningEffort.

Also fixed: the settings page drew an unset agents.defaults.reasoningEffort as
Low while the provider's own default was what ran. The unset state is now its own
rung, Provider default, and the key accepts null so that rung clears it.

Docs: a "Set the reasoning effort" section in the using-raven guide (en + zh), a row
in the command reference's task table, CHANGELOG (Added + Fixed), CONTEXT.md gains
Session Reasoning Effort.

Review follow-ups folded in: a fork keeps its parent's pick with its model; the
agent's raven_config door reads REASONING_EFFORTS and accepts null, as the
settings page does; /effort is refused inside a TUI direct chat (the pick is the
main conversation's); a draft pick whose read fails is put back; the re-read after a
promotion carries the promotion's ticket; the tier row's tooltip names sub-agents
only; and the exhaustion wrap-up and watch-work calls are pinned to the effort in
force by tests that kill the three surviving mutants.

Out of scope, deliberately: the xhigh/max rungs some providers accept (a separate
small PR), and the sub-agent inheritance of the pick.

Type

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

Verification

  • pytest tests/test_session_effort.py tests/test_rpc_session.py tests/test_agent_loop_usage_sink.py tests/test_rpc_settings.py tests/test_rpc_schema_match.py tests/test_rpc_registration.py -- pass

  • full pytest tests -- see the first comment for the count and the baseline failures

  • scripts/coverage_gate.py diff --base-ref origin/main -- see the first comment

  • ruff check / ruff format --check / ty check on the touched modules / lint-imports -- clean

  • ui-web: npm run gen (contract regenerated), vitest run (261 files, all gates), type-check, lint -- clean

  • ui-tui: npm run gen:rpc, npm run gen:i18n, vitest run (146 files), type-check, lint -- clean

  • mkdocs build --strict -- clean

  • The picker row was checked on the built page's offline library (?stub=1) in light, dark and zh

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered -- the new method writes one enum word (or null) to a session's own metadata; the words are checked server-side
  • Backward compatibility considered -- an older page or TUI never calls the new method; a page on an older gateway gets -32601 on the read and hides the row; session.info gains an optional field
  • Rollback path is clear for risky changes -- revert the PR; a record carrying the key is ignored by a build without the reader

Related Issues

N/A

…session.set_effort)

One conversation can now ask for more or less thinking than the configured
default without moving that default. The pick rides the session's metadata
beside `model` (`raven.agent.session_effort`), so it outlives the gateway and
a resumed conversation runs at what it was left at; the turn entrance hands
it to `_run_agent_loop`, which ranks it over the mode's effort and the
configured `agents.defaults.reasoningEffort`, under a hook's rollback
override. Raven's own calls only: sub-agents keep the configured default.

Surfaces: `session.set_effort` (report, set or clear; every answer names what
the default is on this install and the words), `session.info` carries
`reasoning_effort` for the status bar, the WebUI's model picker gains an
effort row under the tier row, and the TUI gains `/effort`.

The settings page drew an unset `agents.defaults.reasoningEffort` as Low; the
unset state is now its own rung, Provider default, and the key accepts null
so that rung clears it. The using-raven guide documents both levels.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw
Handsome-wzw requested a review from LivXue as a code owner October 26, 2026 07:56
@Handsome-wzw
Handsome-wzw requested a review from 0xKT October 26, 2026 07:57
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

Verification on the head commit (local, this Mac):

  • Full pytest tests: 35567 passed, 184 skipped, 2 xfailed, 4 failed. Three of the four fail the same way on a clean origin/main worktree with the same venv (tests/test_rpc_files.py::test_a_host_without_libreoffice_says_so, tests/test_simulation_scenario.py::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runs, tests/test_simulation_suite.py::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence), so they are this machine's baseline. The fourth was tests/test_rpc_session_init_bundle.py::test_default_session_info_key_set_matches_expected_v030, the anti-drift gate on the init bundle's key set -- amended into the commit with reasoning_effort added to the expected set.
  • Diff coverage: 98.84% (85/86 executable changed lines); the one uncovered line is the dispatcher closure _set_effort in register_session_methods, reached only through a live dispatcher.
  • ruff check, ruff format --check, ty check on the touched modules, lint-imports: clean.
  • ui-web: npm run gen (contract regenerated), vitest run 257 files / 4392 tests incl. every gate, type-check, lint (0 errors): clean.
  • ui-tui: npm run gen:rpc, npm run gen:i18n, vitest run 146 files / 2285 tests, type-check, lint (0 errors): clean.
  • mkdocs build --strict on the docs site: clean.
  • The picker row was checked on the built page's offline library (?stub=1) in light, dark and zh: the row sits under the tier row, the five rungs share one line under their label, the help mark names what Default stands for.

@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.

Blocking: the Web UI loads the previous conversation's reasoning effort when switching sessions.

I found one blocking issue, marked inline, and no other actionable findings. I covered the repository rules and canonical terms, the full diff, affected callers and the single-commit history, RPC/backward compatibility, the runtime/Web/TUI architecture boundaries, and the test changes; I found no weakened tests.

Verification after initializing this worktree from its lockfiles (uv sync --extra dev, npm ci in both frontends):

  • uv run pytest tests/test_session_effort.py tests/test_agent_loop_usage_sink.py tests/test_rpc_session.py tests/test_rpc_session_init_bundle.py tests/test_rpc_settings.py -q: 416 passed
  • uv run pytest tests/test_rpc_schema_match.py -q: 517 passed
  • Web focused Vitest: 140 passed; npm run type-check: passed
  • TUI focused Vitest through npm test -- ...: 11 passed; npm run type-check: passed
  • git diff --check github/main...HEAD: passed

The first attempts before dependency initialization failed during setup (raven_everos, vitest, and esbuild were absent); the commands above are the completed reruns.

Comment thread ui-web/src/state/session/registry.ts Resolved
@LivXue

LivXue commented Oct 26, 2026

Copy link
Copy Markdown
Member

Windows compatibility review — PR #1113 @ aaa07a7db6b93eaf8913f803aa1e9a617d5f983f

Scope: base 018f984dd963 → this head. Read the Python/RPC/config side in full (raven/agent/session_effort.py, raven/agent/loop/turn_path.py, raven/rpc/methods/session.py, raven/rpc/methods/console.py, raven/config/schema.py, raven/rpc/models.py), the generated RPC/i18n files, and the changed Web/TUI tests; existing threads read.

Result: no Windows-specific issue found.

What I checked:

  • The new session_effort module only reads/writes the session record's existing metadata dict through the existing session manager (peek, append_metadata_patch, get_or_create); it introduces no new file path, atomic-write, encoding, subprocess, or platform primitive. Persistence and its failure fallback are the manager's, unchanged.
  • turn_path.py's effort precedence is pure in-memory logic; REASONING_EFFORTS and the RPC models are constants/validation.
  • The Web effort state, picker, roles and settings source changes are platform-neutral TypeScript.

Verification (native Windows 11, CPython 3.12.13, isolated worktree D:\tmp\rv-1113 at this head, scratch HOME/USERPROFILE, PYTHONUTF8=1):

pytest tests/test_session_effort.py tests/test_rpc_session.py \
       tests/test_agent_loop_usage_sink.py tests/test_rpc_settings.py \
       tests/test_rpc_session_init_bundle.py -q
→ 1 failed, 415 passed in 22.23s

vitest run src/state/effort.test.ts src/features/model/ModelPicker.test.tsx \
            src/features/settings/providers/Roles.test.tsx src/features/settings/source.test.ts
→ 4 files, 140 passed

The one Python failure is inherited, not this PR: test_rpc_settings.py::test_a_write_to_a_root_the_user_manages_is_refused_readably uses Path("/somewhere/theirs") as a regex match against the error message, which renders as \somewhere\theirs on Windows. It fails identically at the base 018f984dd (verified there), and the test is unchanged by this PR.

Limitations: ui-tui tests were not run — this checkout has no usable ui-tui/node_modules for the test harness; the full suite was not run.

Handsome-wzw and others added 2 commits October 27, 2026 04:17
… left

A switch asks for the conversation being opened before the page's current
session moves over to it, and the effort read built its request from "the
current session": opening B from A asked for A's effort and painted it on
B -- and a pick of that very rung on B was taken as already in force. The
read now names the conversation and the switch it belongs to, the way the
permission-mode read does, and the source drops an answer whose switch has
been overtaken rather than painting a view the reader has left.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>

@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.

The prior session-switch blocker is fixed in 516c7e7d4284; I replied on and resolved that thread. I reviewed the focused fix commit separately from the merged main history, its callers and regression tests, the Web UI source/store/session architecture boundary, backward compatibility, repository rules and canonical terms, and the test changes; no tests were weakened and I found no new actionable issue.

Verification:

  • npx vitest run src/features/model/source.test.ts src/state/effort.test.ts src/state/session/registry.test.ts src/features/model/ModelPicker.test.tsx: 159 passed
  • npm run type-check: passed
  • npm run lint: 0 errors, 4 pre-existing warnings in untouched files
  • git diff --check github/main...HEAD: passed

@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

On the blocking thread at ui-web/src/state/session/registry.ts (the Web UI loading the previous conversation's effort when switching), fixed in 516c7e7, after merging origin/main as a4bedeb (the one conflict was CHANGELOG.md, both sides kept):

  • The read now names the conversation being opened and the switch it belongs to, the way loadPermMode does: switchTo calls loadEffort(s.id, gen) and the draft path loadEffort(null, gen); effortSource.read(sid, gen) builds the request from that key and answers null once the switch's ticket has been spent, so an answer about a view the reader has left paints nothing.
  • Pinned by registry.test.ts (opening B from A reads B's key, and the read is issued before the current session moves over; leaving for a draft reads with no key) and source.test.ts (an overtaken read resolves to null; a standing one answers; a draft reads with no key), plus two cases in state/effort.test.ts.
  • Verification on the new head: ui-web vitest run 258 files / 4415 tests including every gate, type-check and lint (0 errors) clean; gen:check, lint:rpc, lint:i18n unchanged after the merge; the three repository gates (commit messages, source language, large files) pass.

The tier row next door reads the same way (loadTier() asks for the current session before the switch-over); that predates this PR and is left as is here, for a follow-up of its own. I could not reply inside the thread from this session, so this comment stands in for it; the thread is left open for the reviewer to close.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review — PR #1113 @ 516c7e7d4284b5d96f8dd99194cef8f2c5d8f5e7

Scope: the delta from the previously reviewed head aaa07a7d. The 69-file diff includes main's merge; the PR's own 47 files changed in 11 of them (+160/−15): ui-web/src/features/model/source.ts, state/effort.ts, state/session/{registry,runtime}.ts, their tests, i18n/CSS/CHANGELOG. Read the complete own-file delta; the Python/RPC side of the PR is unchanged across it.

Result: no Windows-specific issue.

What changed: the effort read is now keyed to the conversation being opened and the session-switch generation (read(sid?, gen?), null when the switch was overtaken), matching the permission-mode pattern, instead of reading "whichever session is current" before the switch lands. That is browser state logic only — no paths, filesystem, subprocess, encoding, or platform primitive.

Verification (native Windows 11, isolated worktree D:\tmp\rv-1113-n at this head; scratch HOME/USERPROFILE, PYTHONUTF8=1; node_modules borrowed read-only from the main checkout):

pytest tests/test_session_effort.py tests/test_rpc_session.py \
       tests/test_agent_loop_usage_sink.py tests/test_rpc_settings.py \
       tests/test_rpc_session_init_bundle.py -q
→ 1 failed, 415 passed        (the failure is the inherited POSIX `/somewhere/theirs` regex test)

vitest run src/state/effort.test.ts src/features/model/source.test.ts \
            src/state/session/registry.test.ts src/features/model/ModelPicker.test.tsx \
            src/features/settings/providers/Roles.test.tsx
→ 5 files, 196 passed

The one Python failure fails identically at base 4718421324 (POSIX path literal matched against a Windows-rendered message) and is unchanged by this PR.

Limitations: TUI tests not run (its harness is unavailable in this checkout); no real-browser walkthrough.

Comment thread raven/agent/loop/turn_path.py Resolved
@0xKT

0xKT commented Oct 27, 2026

Copy link
Copy Markdown
Member

Not a blocker -- smaller things from the same pass, separate from the blocking thread. Each was read at this head; none was executed.

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.

  1. /effort inside a TUI Direct Chat (ui-tui/src/app/slash/commands/session.ts:652) always sends session_key = ctx.sid, which is the main session. Its reply, "reasoning effort: high (this conversation; ...)", then lands in the sub-agent's pane. The user is told "this conversation" changed, but the main conversation's persisted effort is what moved. /mode handles the direct-chat target (core.ts:331-351). Refusing there, or naming the main conversation in the reply, would fix it.

  2. A draft pick whose call fails stays staged (ui-web/src/features/model/source.ts:403). staging().effort is written before the session.set_effort read that can fail. On failure the toast says the effort was not set and the old rung is repainted, but the staged value is never rolled back. The first send's applyStagedEffort (runtime.ts:671-674) then runs the turn at it. This is reachable whenever the socket is down, for example during a gateway restart.

  3. The re-read after promotion takes the current view's ticket (ui-web/src/state/session/runtime.ts:678). void loadEffort(sessionId) passes no generation, so if the reader opened B while the promotion's write was in flight, S's effort can be painted on B's row. That is the switch race 516c7e7 fixed, now on the promotion path. applyStagedModel carries the promotion's ticket for exactly this case (runtime.ts:553/598).

  4. Two adjacent rows both claim the reasoning depth. The tier row's tooltip gui.tier.scope (i18n/messages.json:6166-6169) says reasoning depth follows the tier, in zh the phrase for "model reasoning depth". The new row directly under it is titled "Reasoning effort", whose zh title is the same "reasoning depth" phrase. In code only the new row moves Raven's own calls; the shipped tiers leave reasoningEffort inherited. Rewording gui.tier.scope to name sub-agents only would remove the overlap.

  5. The agent's raven_config door still has its own word list (raven/config/self_surface.py:151) and refuses null for this key (:181-188, :846-849). After this PR the settings page can clear the key and the agent cannot. Pointing it at REASONING_EFFORTS with nullable=True would give the words one owner.

  6. Nits: a fork carries model but drops the effort pick (raven/session/manager.py:1264-1271). session_effort.write's docstring promises a "pending" report that session.set_effort never makes. /effort keeps its own reset words instead of the RESET_WORDS that /mode uses.

@0xKT

0xKT commented Oct 27, 2026

Copy link
Copy Markdown
Member

Merge conflict: @Handsome-wzw this PR no longer merges cleanly into main.

Measured just now with git merge-tree --write-tree of main 26352da0 and this head 516c7e7d. Main moved with #1110 (Agent Plugins loader), #1064 (conditional branches) and #1066 (DAG file references), each of which changed CHANGELOG.md. Conflicting files: CHANGELOG.md.

After a rebase onto current main I re-check what moved before the grade comes back.

@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.

Blocking: clearing Provider default still sends the gateway boot-time effort.

This head is a merge of current main; its only conflict resolution is in CHANGELOG.md, and it does not change the open Provider-default blocker. In the merged code, a null live default still makes default_reasoning_effort return None, and gen_overrides still omits the key when that value is falsey, so the provider falls back to its frozen boot-time generation setting. The measured reproduction and details remain on the existing open thread; I found no new actionable issue in this revision.

I reviewed the head delta and merge resolution, the blocker path and its callers, relevant architecture boundaries, backward compatibility, repository rules and canonical terms, and test changes; I found no weakened tests.

Verification:

  • uv run pytest tests/test_agent_loop_usage_sink.py tests/test_rpc_session.py tests/test_rpc_settings.py -q: 399 passed
  • git merge-tree --write-tree HEAD github/main: clean
  • git diff --check github/main...HEAD: passed

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review — PR #1113 @ a87fa5922d1c717e2acad19694c8a82a2d44a2d8

Scope: the head moved from the previously reviewed 516c7e7d. Against the new main tip 26352da0, the PR's own change is still the same 47 files, and its raven/** portion is byte-identical to the reviewed head — 6 files, +246/−12 (turn_path.py, session_effort.py, config/schema.py, rpc/methods/{console,session}.py, rpc/models.py). The larger raw delta is main's merges (including #1119's _fill_resumed_context, which is main's code, not this PR's).

Result: no Windows-specific issue; the previous conclusion holds.

Verification (native Windows 11, CPython 3.12.13, isolated worktree D:\tmp\rv-1113-n2 at this head; scratch HOME/USERPROFILE, PYTHONUTF8=1; node_modules borrowed read-only from the main checkout):

vitest run src/state/effort.test.ts src/state/session/registry.test.ts \
            src/features/model/ModelPicker.test.tsx src/features/settings/providers/Roles.test.tsx
→ 4 files, 156 passed

pytest tests/test_session_effort.py tests/test_rpc_session.py \
       tests/test_agent_loop_usage_sink.py tests/test_rpc_settings.py \
       tests/test_rpc_session_init_bundle.py -q
→ 1 failed, 430 passed

The single failure is the inherited POSIX /somewhere/theirs regex test, unchanged and failing identically at base. The delta added only main's code, and the effort feature the PR contributes has no path, subprocess, signal, encoding, or platform primitive.

Limitations: TUI tests not run (harness unavailable in this checkout); no real-browser walkthrough.

…xt turn

Picking Provider default clears agents.defaults.reasoningEffort, and
session.info / session.set_effort reported the clear, but a gateway that
booted while the key held a word kept calling at that word: the turn left
reasoning_effort out of the call when the live reader answered None, and
the provider filled the missing argument from its own copy of the key,
frozen at construction (ResolvingProvider). The display and the wire
disagreed in exactly the way the per-conversation effort set out to end.

The turn now sends the effort in force on every call, None included: an
explicit None reaches the provider as None (its own default, nothing on the
wire) instead of the boot copy, so a clear lands on the next turn the way a
word change already did. The exhaustion wrap-up and the watch-work
judgement send it the same way.

The ranking has one home, session_effort.in_force (pick > mode's word >
configured default, None one answer with one meaning), which the turn
runs at through session_reasoning_effort on the loop and the RPC reports
from, the way session_tier is shared; the two used to spell it out apart
and to read an absent word differently.

Tests: a real provider booted at high with the live key cleared records
None on the next turn (low as the control), the three cases that pinned
"the key is left out" now pin "the key is sent as None", and in_force has
its own unit tests. CONTEXT and the CHANGELOG entry say the rule.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

On the blocking thread at raven/agent/loop/turn_path.py (clearing Provider default still sent the gateway's boot-time effort): fixed in 5057b49, following the two points in the thread.

  • The turn sends the effort in force on every call, None included. gen_overrides always carries reasoning_effort; an explicit None reaches the provider as None (chat_with_retry resolves only the sentinel from generation.reasoning_effort), which is nothing on the wire -- the provider's own default -- instead of the copy frozen at boot. The exhaustion wrap-up (_synthesize_final_on_exhaustion) and the watch-work judgement (_note_watch_work) send it the same way; both used to leave the key out on None too.
  • One home for the ranking: session_effort.in_force(pick, mode_effort, configured) in raven/agent/session_effort.py. The turn runs at it through session_reasoning_effort(session_key, pick) on the loop (beside session_tier, for its reason), and session.info / session.set_effort report from it (_default_effort / _effort_in_force in rpc/methods/session.py are now that call over the fresh config). None is one answer with one meaning on both sides.

Tests, as the thread asked: test_a_default_cleared_on_a_running_gateway_reaches_the_next_turn boots a real LLMProvider subclass with GenerationSettings(reasoning_effort="high"), points the loop's LiveConfig at a file holding reasoningEffort: null, runs one turn and asserts the provider's chat saw None; the low control asserts low. The three cases that pinned the old "the key is left out" (test_agent_loop_session_effort.py x2, test_agent_loop_iteration_hooks.py, plus test_watch_work.py's) now pin "sent as None". in_force has two unit tests. On the previous source with the new tests kept: 7 fail (session_reasoning_effort / in_force missing, the booted provider recording high); with the change: green.

Verification on this head: the 16 related test files (test_agent_loop_session_effort, test_session_effort, test_agent_loop_iteration_hooks, test_watch_work, test_rpc_session, test_rpc_settings, test_rpc_session_init_bundle, test_agent_loop_usage_sink, the three research-flow effort files, test_agent_loop_context_overflow, test_subagent_third_party, test_config_live, test_agents_research_mode_tiers, test_context_compaction_trigger) -- 1096 pass; pytest -k "reasoning or effort" over the whole tree -- 165 pass; the full suite -- 36231 pass, 259 skipped, 11 fail. 3 of the 11 are this Mac's standing baseline (test_rpc_files::test_a_host_without_libreoffice_says_so, test_simulation_scenario::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runs, test_simulation_suite::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence, failing identically on main). The other 8 are all of tests/test_agents_oncall_flow_submit_tool.py and are a cross-test pollution that is main's, not this branch's: OpsSubmitTool.execute takes _occ_home = led.parent.parent, and the tests put the ledger at tmp_path/l.json, so the occupancy scan walks the pytest basetemp and reads every other test's tmp_path as a sibling campaign; tests/test_agent_plugins_ledger.py::test_load_ignores_a_file_that_is_not_an_object (new on main today, from #1110) leaves tmp_path/ledger.json holding ["demo-pack"], and Ledger._load raises AttributeError on payload.get for a list, which duplicate_refusal does not catch (it catches OSError, ValueError, LedgerCorruptError). Reproduced by order with -n 0: pytest tests/test_agent_plugins_ledger.py tests/test_agents_oncall_flow_submit_tool.py -> 8 fail; the reverse order -> 32 pass; the file alone -> 21 pass. Neither file nor the oncall plugin is touched by this PR; I will file the isolation fix separately; ruff check / ruff format --check clean on the changed files; lint-imports 10 kept, 0 broken; check_commit_messages, check_source_language, commitlint on the new commit all exit 0. CONTEXT.md's Session Reasoning Effort entry and the CHANGELOG entry now state the rule (None is sent, the boot copy is never consulted). Docs for the key (using-raven.md: "Left unset, the provider's own default applies"; "Provider default clears the key") already describe the fixed behaviour.

The six non-blocking points from the same pass are not in this commit; I can take them in a follow-up on this branch or in a small PR, whichever the reviewer prefers.

@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

Merged origin/main (984d47d, #1124) into the branch as 01f2455, no rebase: the one conflict was CHANGELOG.md, where both sides added an entry at the top of ### Added; both are kept, this PR's entry first and #1124's after, so the diff against main is only this branch's own entries. i18n/messages.json, the generated TUI catalogue and ui-web/src/app/install.ts auto-merged; gen:i18n regenerates the identical file and lint:i18n / gen:check are in sync.

On the merged tree: ui-web vitest run 261 files / 4448 tests pass, type-check clean, lint 0 errors; ui-tui npm test 146 files / 2285 pass, type-check clean; Python catalogue readers plus the effort files 114 pass; check_commit_messages, check_source_language, check_large_files and commitlint on each non-merge commit exit 0. The diff against main is 52 files, none of them #1124's.

The head to review is still 5057b49 for the fix itself (the comment above); this commit only carries main.

@Handsome-wzw
Handsome-wzw requested a review from 0xKT October 27, 2026 08:46

@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.

The Provider default blocker is fixed: the shared ranking now supplies the turn and RPC surfaces from one rule, and every model-call path forwards the resolved value, including explicit None. The real-provider regression covers the exact boot-high/live-null failure and its low control.

I reviewed the delta from the prior head and the resulting github/main...HEAD change, the provider sentinel and streaming callers, RPC reporting, hook/auxiliary calls, history and backward compatibility, the AGENTS.md and CONTEXT.md constraints, and whether tests were merely weakened. The latest-main merge leaves the feature implementation unchanged; its manual CHANGELOG resolution preserves both sides. No new plain error survived refutation.

Verification on the exact head: uv run pytest across the touched session-effort, loop, RPC, watch-work, usage-sink, provider-reasoning and LiteLLM streaming suites: 587 passed, with 26 dependency warnings. git diff --check passed.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review — PR #1113 @ 01f24551809d7cb5ed8a706b585eab00959c8e4e

Scope: the head moved from the previously reviewed a87fa592, and — unlike that move — this one adds code: raven/agent/session_effort.py gains in_force() (the one ranking rule: pick → mode → configured → None), wiring.py/turn_path.py send reasoning_effort as an explicit value including None so the provider's frozen boot copy is not consulted, and organ_glue.py/rpc/methods/session.py use the same rule for the watch-work judgement and the session.info report. The PR's raven/** diff is now 8 files, +311/−22 (main tip 984d47d4).

Result: no Windows-specific issue. The change is effort-selection and reporting logic — strings and None passed through the existing call path, with no filesystem, subprocess, signal, encoding, or platform primitive.

Verification (native Windows 11, CPython 3.12.13, isolated worktree D:\tmp\rv-1113-n3 at this head, scratch HOME/USERPROFILE, PYTHONUTF8=1):

pytest tests/test_session_effort.py tests/test_rpc_session.py \
       tests/test_agent_loop_usage_sink.py tests/test_rpc_settings.py \
       tests/test_rpc_session_init_bundle.py -q
→ 1 failed, 432 passed

The single failure is the inherited POSIX /somewhere/theirs regex test, unchanged by this PR and failing at base. The UI files are untouched by this delta, so the earlier vitest results still stand.

Limitations: TUI tests not run; no live provider call (the explicit-None wire behaviour is covered by the provider-protocol/usage-sink tests).

if not isinstance(metadata, dict):
return None
stored = metadata.get(METADATA_KEY)
return stored if isinstance(stored, str) and stored in REASONING_EFFORTS else None

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.

Blocker - derive the effort options from the conversation's model, not from one global word list. session.set_effort returns "options": list(REASONING_EFFORTS) (raven/rpc/methods/session.py:1653) and validates against the same tuple (:1663), and session_effort.of() drops any stored word outside it. All three are model-blind, so every conversation is offered minimal/low/medium/high whatever its model accepts. A Codex conversation can therefore store minimal; openai_codex_provider.chat forwards the word verbatim in body["reasoning"] with no mapping and no fallback, and no Codex model accepts it: every entry in this machine's ~/.codex/models_cache.json (fetched 2026-10-27 from the same chatgpt.com/backend-api/codex/models endpoint codex_catalog.py calls) lists low/medium/high/xhigh/max, none lists minimal. The rejection is measured, not inferred: on 2026-10-20 at main 2e390adff the Codex backend answered HTTP 400 "only low/medium/high/xhigh/max are supported" for minimal (session tui:20261018_143429_bfe12b). I did not re-run a Codex turn at this head; the wire path here is code-reading. The reverse gap is the same defect: xhigh, max and ultra are supported by every Codex model and cannot be stored, because of() and the setter reject them. On the Anthropic path _ADAPTIVE_EFFORTS maps minimal and low to the same wire value, so two of the five offered options send the same request.

The data is already in hand: codex_catalog.py::_visible_slugs keeps only slug from each entry and drops supported_reasoning_levels and default_reasoning_level, and the model registry already has a reasoning capability tag. Remedy direction: compute options from the conversation's current model (the Web row and the TUI draw the server's list, so no client change is needed), have the setter and of() accept the same list, and decide what a model switch does with a pick the new model does not accept.

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.

Blocking: per-conversation effort options must not allow a value that the conversation's model rejects.

I confirmed the concrete chain on 01f24551809d: session.set_effort advertises and accepts minimal from the global tuple, and a credential-free payload probe of OpenAICodexProvider.chat(..., reasoning_effort="minimal") captured {"effort": "minimal"} unchanged on the Codex wire. That corroborates the reported HTTP 400 rather than refuting it.

The focused RPC/session/Codex suite still passes (17 passed), but those tests assert the global list and do not cover model-specific acceptance. This new evidence withdraws my clean stance; I am leaving resolution to the reviewer who opened the thread.

on_token_delta=on_token_delta,
on_reasoning_delta=on_reasoning_delta,
reasoning_effort=policy.reasoning_effort,
reasoning_effort=turn_effort,

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.

Nonblocking - pin the two side calls to the live effort; the code is right, but no test fails if it regresses. The main call now forwards turn_effort explicitly, and the wrap-up (_synthesize_final_on_exhaustion, called at :2151) and the watch-work judgement (_note_watch_work, called at :1572) share the shape by construction. Nothing pins it: in an isolated copy of this head I reverted the watch call site to policy.reasoning_effort (M1), the exhaustion call site the same way (M2), and the wrap-up helper at :648 to omit the key on None (M5); all 980 tests across the 25 related files stayed green. The controls behave: reverting the main call at :1230 fails 4 tests (including the new real-provider regression) and the same change in organ_glue.py:615 fails 1. So the two paths this PR's earlier blocker was about are correct today and unpinned tomorrow. Remedy direction: one assertion per side call that the provider received the session's live value, including the cleared case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pinned at 1a0e9c2 (tests/test_agent_loop_session_effort.py): each side call is driven to a real provider booted at high under the configured default where the policy names none, the session's own pick, and a cleared key that must arrive as an explicit None. Your three mutants, re-applied at this head: M1 (:1572) fails the watch-work test's configured-default and own-pick cases; M2 (:2151) fails the wrap-up test's same two; M5 (:648) fails the wrap-up test's cleared case, where the provider records high. Details in the top-level comment.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member

Blocking: 1 finding(s) need correction before this revision can merge; see the inline notes.

Scope: I reviewed only the raven/agent/** portion of this PR -- 4 of 52 changed files (session_effort.py, turn_path.py, wiring.py, organ_glue.py). The remaining 48 files were read as context but are not covered by this verdict. Rings 3 and 4 (raven/agent/subagent/**, the dispatch tools and the DAG skill) are untouched by this PR.

This round covers the delta from a87fa592 to 01f24551809d, whose substance is 5057b49f (the Provider-default fix) on top of current main. I confirm that fix: every model call now carries the effort in force, explicit None included, resolved through one ranking function session_effort.in_force; the loop reads it via session_reasoning_effort and the RPC reports from the same call. I mutation-tested it at this head: reverting the main call at turn_path.py:1230 to the sentinel-omitting form fails 4 tests, including the author's real-provider regression; the analogous change in organ_glue.py:615 fails 1. gloryfromca's and 0xKT's blockers on that path are closed as far as I can tell. The two side calls are correct but unpinned (F1).

Confirmed findings:

  • Blocker: session.set_effort offers and validates one fixed four-word list for every model, and session_effort.of() drops anything outside it. A Codex conversation can store minimal, which the Codex backend answers with HTTP 400; xhigh, max and ultra, which Codex models do support, cannot be stored. The catalogue Raven already fetches carries the per-model levels and discards them.
  • Nonblocking: the exhaustion wrap-up and watch-work calls forward the live effort in code, but no test fails when either call site is reverted or when the wrap-up helper omits the key on None (three surviving mutants).

Verification:

  • uv run pytest over 25 related test files at 01f24551809d, in an isolated copy of the head: 980 passed. Mutants: turn_path.py:1230 killed by 4 tests, organ_glue.py:615 killed by 1; turn_path.py:1572, :2151 and :648 survive.
  • F2 is code-reading plus this machine's ~/.codex/models_cache.json; the 400 quoted above was measured on 2026-10-20 at main 2e390adff (session tui:20261018_143429_bfe12b), not re-run at this head.

Checked and deliberately not reported: the undocumented NANOBOT_* environment-variable channel. With effort set only through the environment, the explicit None this PR sends to the provider overrides it, while the RPC reports the file value. No documentation, test, or in-repo use refers to that channel, so I am not filing it; say the word if you want it treated as a defect.

@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.

Blocking: per-conversation effort options must not allow a value that the conversation's model rejects.

The new model-capability evidence changes my stance on this unchanged revision. I confirmed that the RPC offers and stores minimal without consulting the conversation's model and that the Codex provider forwards it verbatim, matching the reported backend rejection. The existing 17 focused tests pass but do not exercise model-specific effort acceptance.

This is the outstanding blocker from the open thread at raven/agent/session_effort.py; I added measured corroboration there and did not resolve a thread opened by another reviewer. The prior Provider-default fixes remain verified.

Handsome-wzw and others added 3 commits October 27, 2026 21:19
…ords

Inside a direct chat with a sub-agent instance, /effort sent the main
conversation's session_key and then said "this conversation" in the
instance's pane: the main record moved while the reader was told the
instance's had. The pick is the main conversation's and an instance has
none of its own to set from here, so the command is refused there, naming
the instance and the way back, the way /instance does. The reset words are
/mode's RESET_WORDS now rather than a second copy of the same three.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…omotion's ticket

A draft's effort pick was staged before the session.set_effort read that
answers the default and the words, and left staged when that read failed:
the toast said the pick was not made and the old rung was repainted, but
the first send's applyStagedEffort ran the turn at it. The pick is still
staged first, so a send during the round trip carries it, and a failed
read puts back the last pick whose read landed -- not merely the value
staged before the failing call, which is another in-flight pick when two
went out and both failed.

The re-read after the promotion wrote the staged effort took the view's
ticket after the awaited write, so a reader who opened another
conversation while the write was in flight got the minted session's rung
painted over that conversation's own read. The promotion's ticket now
rides into applyStagedEffort the way it rides into applyStagedModel, and
the source drops the late answer.

The tier row's tooltip (gui.tier.scope) said reasoning depth follows the
tier, one row above a row titled with the same words; the shipped tiers
leave the effort alone, so the tooltip now names sub-agents only. The TUI
catalogue is regenerated from it.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…arry the pick on a fork

The exhaustion wrap-up and the watch-work judgement forward the effort in
force, explicit None included, but nothing failed when either call site
went back to the policy's word or the wrap-up helper left the key out on
None. Two tests now drive each side call to a provider booted at high:
the configured default where the policy names none, the session's own
pick, and a cleared key that must arrive as None rather than be filled
from the boot copy. The watch-work case is pinned at the provider, through
chat_stream, with the spawn roster naming an on-call agent so the look
earns the judgement.

The agent's raven_config door kept its own copy of the effort words and
refused null for agents.defaults.reasoningEffort while the settings page
accepted it. The setting reads REASONING_EFFORTS and is nullable, so the
words have one owner and the agent can clear the key the way the page
does.

A fork carried the parent's model but dropped its effort pick, so a forked
conversation ran at the default until resumed; FORK_CARRIES names what a
fork carries, with the pick beside the model. session_effort.write's
docstring promised a "pending" report that session.set_effort never makes;
it now says what the answer is for. CONTEXT.md, the using-raven guide and
the CHANGELOG entry carry the three user-visible facts.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

The non-blocking items from the two passes are in, as three commits on this branch (TUI 251cdb551, Web bffb9803f, Python + docs 1a0e9c2e6; head 1a0e9c2e6). LivXue's blocker on the per-model word list is left alone on purpose: it overlaps #1123 and the split is still to be decided, so nothing here touches how the words are chosen.

LivXue F1 -- the two side calls are pinned (tests/test_agent_loop_session_effort.py). Two parametrized tests drive each side call to a real LLMProvider booted at high, under three readings of the effort in force: the configured default where the policy names no word (low), the session's own pick (high on the record), and a cleared key (reasoningEffort: null), which has to arrive as an explicit None rather than be filled from the boot copy. test_the_wrap_up_runs_at_the_effort_in_force_not_the_policys_word runs a turn to the iteration cap and asserts both calls the provider saw; test_the_watch_work_judgement_is_paid_at_the_effort_in_force patches the spawn roster with an on-call agent so a list_dir earns the judgement, and asserts the three calls -- the ReAct step, the judgement through chat_stream, the step after -- all carried the live value. Mutants, each applied to turn_path.py at this head and reverted:

  • M1 (watch call site back to policy.reasoning_effort, :1572): killed by the watch-work test's configured-default and own-pick cases (2 fail, 107 pass over the three loop/watch files).
  • M2 (wrap-up call site back to policy.reasoning_effort, :2151): killed by the wrap-up test's configured-default and own-pick cases (2 fail).
  • M5 (wrap-up helper omits the key on None, :648): killed by the wrap-up test's cleared case (1 fail; the provider recorded high).

0xKT's six:

  1. /effort inside a TUI direct chat is refused rather than redirected: the pick is the main conversation's and an instance has no effort of its own to set from here (/mode has one per instance; this does not). The line names the instance and the way back; nothing is sent. effortCommand.test.ts runs /effort, /effort high and /effort default inside enterDirect('Raven-Code', 'refactor-auth') and asserts the refusal, no RPC, status bar untouched. The using-raven guide (en + zh) and CONTEXT.md say so.
  2. A draft pick whose session.set_effort read fails is put back (features/model/source.ts). Still staged before the read, so a send during the round trip carries it; on failure the source restores the last pick whose read landed -- not merely the value staged before the failing call, which is another in-flight pick when two went out and both failed. Three cases in source.test.ts (a draft's effort pick): stage-then-fail puts it back, a landed read keeps it, and the double-failure case ends on high, not medium.
  3. promote passes its ticket into applyStagedEffort(draftRt, s.id, gen) -> loadEffort(sessionId, gen), the shape fix(ui-web): read the opened conversation's tier, not the one being left #1122 gave the tier. Two cases in source.test.ts (the staged effort write): a write overtaken by a switch records {sid: 'a', gen: began} with began spent and the source answering null under it; a reader who stayed records the same read under a ticket that still stands.
  4. gui.tier.scope now reads "The tier the sub-agents this conversation dispatches run at. This conversation only.", and the zh line says the same without the words the effort row is titled with; the TUI catalogue is regenerated (gen:i18n, lint:i18n in sync).
  5. raven_config's setting for agents.defaults.reasoningEffort reads REASONING_EFFORTS and is nullable=True (self_surface.py); the private four-word tuple is gone. test_config_self_surface.py: test_the_reasoning_effort_words_have_one_owner holds the setting's choices to REASONING_EFFORTS and checks every word and null against the console's own checker; test_check_value gains null / high / ultra rows; the exemption for this key in test_every_concrete_path_is_a_schema_field is removed. test_raven_config_tool.py: a null set goes through the lent caller as {"value": None}, and through the real settings_set it lands as null in the file.
  6. A fork carries the pick with the model (FORK_CARRIES in session/manager.py; the key is spelt there rather than imported, and test_the_fork_carries_the_key_the_effort_pick_is_kept_under holds it to session_effort.METADATA_KEY); a cleared pick (null on the record) is not carried. session_effort.write's docstring no longer promises a "pending" report. /effort uses /mode's RESET_WORDS, now exported from core.ts, and the TUI test iterates that set.

Not changed, as LivXue noted and left unfiled: with effort set only through the NANOBOT_* environment channel, the explicit None this PR sends overrides it while the RPC reports the file value. No test, doc or in-repo use refers to that channel; it is left as is.

Verification at 1a0e9c2e6:

  • Python: the 11 related files (session_effort, agent_loop_session_effort, agent_loop_iteration_hooks, watch_work, rpc_session, rpc_settings, rpc_session_init_bundle, agent_loop_usage_sink, config_self_surface, raven_config_tool, session_manager) -- 958 pass; full pytest tests -n auto -- 36255 pass, 258 skipped, 2 xfailed, 3 fail, the 3 being this Mac's standing baseline (test_rpc_files::test_a_host_without_libreoffice_says_so, test_simulation_scenario::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runs, test_simulation_suite::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence, failing identically on main); scripts/coverage_gate.py diff over this round's changed lines -- 100% (3/3 executable changed lines); ruff check / ruff format --check clean; lint-imports 10 kept, 0 broken.
  • ui-web: vitest run 261 files / 4453 tests incl. every gate -- pass (a first run concurrent with pytest -n auto hit two 5 s timeouts and one 160 ms timing assertion in registry.test.ts, runtime.promote.test.ts and ExtAgentsPage.test.tsx; the three files pass alone and the whole suite passes re-run without the load); type-check clean; lint 0 errors (the 4 standing warnings).
  • ui-tui: npm test 146 files / 2288 tests pass; type-check clean; lint 0 errors; lint:i18n, lint:rpc in sync.
  • mkdocs build --strict clean; check_commit_messages, check_source_language, check_large_files and commitlint on each commit exit 0.
  • Docs against code: CONTEXT.md's Session Reasoning Effort entry (fork, raven_config, direct chat), session_effort.write's docstring, the using-raven section (en + zh), the CHANGELOG entry and the tier tooltip were read against the code after the changes; the one contradiction found was the docstring's "pending" promise, fixed above.

A short reply sits in LivXue's F1 thread as well; the threads are left for their authors to close. #1122 and #1123 sit on this branch and will need this head merged in.

@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; suggestions only, and they are marked inline.

The new revision correctly addresses the accepted follow-ups: the wrap-up and watch-work tests now exercise the live configured value, session pick, and explicit-None clear through a real provider; raven_config can clear the global key; forks carry a non-null conversation pick; TUI Direct Chat refuses /effort instead of mutating the main session; and the Web draft/promotion races restore the last landed pick and retain the promotion ticket. I found no new plain error in those paths.

I covered the 01f24551809d..HEAD delta and github/main...HEAD relationship, AGENTS.md/CONTEXT.md constraints, the changed callers, persistence/restart and concurrency compatibility, history, and whether tests were weakened. Verification passed: 439 focused Python tests, 45 Web tests, 11 TUI tests, both UI type-checks, and git diff --check.

Named follow-up: the model-blind effort vocabulary remains a real compatibility gap (for example Codex rejects the offered minimal value), but the author has explicitly deferred the ownership/split to #1123. Per that scope decision I no longer hold it as a blocker on this PR; LivXue's thread remains open for its opener.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review — PR #1113 @ 1a0e9c2e66a7516ad25a2eb29001e014d9f3064a

Scope: the head moved from the previously reviewed 01f24551, and this move adds code: the self-surface catalogue now uses REASONING_EFFORTS with nullable=True for agents.defaults.reasoningEffort; a session fork carries reasoning_effort beside model/provider (SessionManager.FORK_CARRIES); the TUI /effort command and the Web effort source/runtime read the pick under the switched conversation; tests grew accordingly (19 files, +395/−42).

Result: no Windows-specific issue. The additions are config-catalogue entries, session-metadata slots (plain strings in a dict), and browser/TUI state logic — no path, filesystem, subprocess, signal, encoding, or platform primitive.

Verification (native Windows 11, CPython 3.12.13, isolated worktree D:\tmp\rv-1113-n4 at this head; scratch HOME/USERPROFILE, PYTHONUTF8=1; node_modules borrowed read-only from the main checkout):

vitest run src/state/effort.test.ts src/features/model/source.test.ts
→ 2 files, 58 passed

pytest tests/test_agent_loop_session_effort.py tests/test_config_self_surface.py \
       tests/test_raven_config_tool.py tests/test_session_manager.py \
       tests/test_session_effort.py tests/test_rpc_session.py -q
→ 644 passed in 16.58s

Limitations: TUI tests not run; no live provider call.

@0xKT

0xKT commented Oct 28, 2026

Copy link
Copy Markdown
Member

Merge conflict: @Handsome-wzw, this PR no longer merges onto the current main tip 959cd0a9 (the #1092 merge). Measured with git merge-tree --write-tree 959cd0a9 1a0e9c2e -> exit 1, one conflict:

  • raven/agent/loop/turn_path.py

The judge's verdict on 1a0e9c2e is clean, so a rebase onto main is what stands between this and the acceptance queue. #1123 sits on this PR and conflicts in the same file, so rebasing this one first keeps the stack in order.

@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.

Blocking: this head must be reconciled with current main because raven/agent/loop/turn_path.py conflicts.

I independently confirmed the new repository state: current github/main is 959cd0a9, the merge base is 984d47d4, and git merge-tree --write-tree HEAD github/main exits 1 with a content conflict in raven/agent/loop/turn_path.py. That makes my prior nonblocking stance stale even though the reviewed head itself has not changed.

The unchanged-head effort baseline still passes: 23 tests across test_agent_loop_session_effort.py and test_session_effort.py. Those results cannot validate the unresolved merged tree; the relevant tests need rerunning after the conflict is reconciled. The model-specific option vocabulary remains the named #1123 follow-up rather than an additional blocker from my side.

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.

4 participants