Skip to content

feat(strategies): follow the core's strategy order, and keep its folders - #445

Merged
guyverino merged 3 commits into
mainfrom
feat/strategy-order-and-folders
Sep 6, 2026
Merged

feat(strategies): follow the core's strategy order, and keep its folders#445
guyverino merged 3 commits into
mainfrom
feat/strategy-order-and-folders

Conversation

@guyverino

@guyverino guyverino commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

What & why

Two halves of one story, both of them things moonproto only started synchronizing in 2e67562
and this terminal had no way to represent before.

Strategy order. A core's strategy list is an arrangement the operator made in MoonBot, and the
library now carries it as the row sequence of a Full snapshot. The tree used to throw it away,
sorting folders into a byte-wise alphabet that put Zeta above alpha and every Cyrillic name
after every Latin one. A folder now sits where its first strategy sits in the core's own list —
taken from the unfiltered list, so typing in the search box cannot rearrange the tree around the
reader.

The operator can also change that order now: two footer buttons, Ctrl+Shift+Up/Down, and two
context-menu items move the selection inside its folder, and the result goes to the core as a
complete id sequence (CoreCmd::ReorderStrategies). Until the core echoes it back the tree draws
the sequence that was sent — without that, a second press would be computed from the arrangement
the first one had already replaced. That same pending sequence is applied to every other
outgoing strategy sync too, or a checkbox toggled in the meantime would hand the core back the
order the operator had just replaced.

Folders, including empty ones. The core keeps a versioned folder tree now, so "new folder" is
a real edit rather than a mark that lives until the window closes. Edits travel as intents — add
this folder, move that subtree — and the complete desired tree is assembled on the feed thread,
where the newest tree is known. That is the whole design point: the wire form deletes every folder
the list omits, so a tree assembled from a window's snapshot turns a create into a silent delete of
whatever arrived meanwhile. A rename sends the rewritten tree together with the rows, or the
emptied old path stays behind on the core as a folder of its own.

Notable decisions

  • Ctrl+Shift+Up/Down, not Alt+Up/Down. The latter ships as the binding for "shift the sell
    order's price" (moon_core::config::hotkeys). One chord with two meanings is a trap even where
    it cannot fire twice today.
  • Two capability flags, not one. CoreFolders::supported means the core keeps a tree and its
    empty folders can be drawn; editable means every path in that tree would also survive
    moonproto's validator. They differ on a real account: MoonBot allows a / inside a folder name,
    moonproto's state then reports the split halves as parent folders, and its validator refuses
    those — so on such a core every folder edit would be refused whole, bundled strategy moves
    included. The terminal asks once and keeps its local marks instead of failing per command.
  • Folder edits are intents, not trees. AddFolder / RemoveFolder / MoveStrategies { rebase }
    carry a path or a subtree; feed::folder_tree applies it to the newest tree the feed knows,
    which is the one it last sent while the core has not answered. feed::folder_tree::sendable is a
    hand-kept mirror of moonproto's validate_strategy_folder_paths, checked line by line against
    handles.rs:1572.
  • A drag now regroups. Moving a strategy between folders relabels the row and joins it to its
    destination's run, because the protocol asks a folder's strategies to stay contiguous and the
    tree places a folder where its first strategy appears — a row left at its old index could drag
    the whole folder somewhere nobody asked for. A rename is deliberately exempt: its anchor is a row
    that was not part of the move, so it relocates nothing.
  • Empty folders are ordered by name. The protocol states outright that their sibling order is
    not synchronized, so there is nothing to follow; MoonBot's own order is its own. Confirmed with
    the owner against a live core.
  • An empty folder draws no caret and no checkbox — both would be live controls that cannot do
    anything — while reserving their space so captions keep their column.

Known limitations

  • A refused reorder or folder edit reaches only the log. The arrangement on screen reverts when its
    confirmation window closes, without saying why; no strategy command has a result path back to the
    window today.
  • analytics/purge.rs still deletes an emptied folder the legacy way. Now that a folder is a thing
    the operator can keep deliberately, that deserves its own look.
  • Empty folders are hidden while a search is running: one matches no query and cannot contain a
    match.
  • Two folders sharing a name at one level are one row here. On the wire a folder IS its path, so
    MoonBot's duplicate nodes cannot be told apart — confirmed with the owner.
  • Local folder marks are not pruned for a core removed from configuration.
  • The folder half needs a core that keeps a folder tree, which CI has none of. Driven by hand on
    BB1: a folder created in the terminal reached the core and came back, and the startup crash above
    was found and confirmed fixed on that same account.

Two startup fixes this branch carries as well

Driving the branch on a real account crashed the application at startup, every launch, before a
window appeared. Both fixes below are independent of the strategy work and needed on main
regardless of it.

  • fix(startup) — rust-i18n builds its backend once, lazily, on the first t!(), and its
    generated initializer materialises all ~2600 keys in ONE stack frame. Whether that frame fits
    depends on where the first lookup happens, which nothing was choosing: it landed deep inside
    chart-tab construction, __chkstk faulted entering the frame, and the process died with
    0xC00000FD. The dictionary crossing that threshold is a shared debt — this branch adds
    thirteen keys, feat(strategies): human labels under Moonbot's own identifiers, every field #441 added the field-label table — but the trigger is the call site, so the same
    size will cross it again from anywhere. Forcing the backend at the base of main builds the
    frame where there is room.
  • fix(diagnostics) — both crash handlers captured a backtrace before writing their record.
    Symbolizing goes through dbghelp, which cannot run on an exhausted stack, so the capture
    faulted and the process died inside the handler with NOTHING written: empty panic.log, empty
    app log, and a Windows crash record blaming dbghelp.dll. Writing the record first is what made
    the overflow above diagnosable at all — the same run then reported its code and address.

How to verify

cargo build -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets
cargo fmt --all -- --check
cargo clippy -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets -- -D warnings
cargo test --workspace --target x86_64-pc-windows-msvc

Green on the rebased tree: moon-core 1654, moon-ui-gpui 1492 + 319 (theme_contract), fmt
clean, and zero new clippy findings in the touched files — the repo's pre-existing clippy debt is
unchanged. FireTest not run: this change touches neither chart, render, windows nor input.

By hand, on a core that keeps a folder tree: rename a populated folder (no ghost left at the old
name), drag an empty folder, delete a populated one (its rows go with it) and an empty one, and
create a folder on an old core (stays local, and its tooltip says so).

Two halves of the same story, both of them things moonproto only started
synchronizing in 2e67562 and this terminal had no way to represent before.

Strategy order. A core's strategy list is an arrangement the operator made in
MoonBot, and the library now carries it as the row sequence of a Full snapshot.
The tree used to throw it away, sorting folders into a byte-wise alphabet that
put Zeta above alpha and every Cyrillic name after every Latin one. A folder now
sits where its first strategy sits in the core's own list, taken from the
unfiltered list so that typing in the search box cannot rearrange the tree.

The operator can also change that order: two footer buttons, Ctrl+Shift+Up/Down,
and two context-menu items move the selection inside its folder, and the result
goes to the core as a complete id sequence. Alt+Up/Down was avoided on purpose —
it ships as "shift the sell order's price", and one chord with two meanings is a
trap. Until the core echoes the new order back, the tree draws the sequence that
was sent; without that a second press would be computed from the arrangement the
first one had already replaced. The same sequence is applied to every other
outgoing strategy sync, or a checkbox toggled in the meantime would hand the
core back the order the operator had just replaced.

Folders. The core keeps a versioned folder tree now, empty folders included, so
"new folder" is a real edit rather than a mark that lives until the window
closes. Edits travel as intents — add this folder, move that subtree — and the
complete desired tree is assembled on the feed thread, where the newest tree is
known: the wire form deletes every folder the list omits, so a tree assembled
from a window's snapshot turns a create into a silent delete of whatever arrived
meanwhile. A rename sends the rewritten tree with the rows, or the emptied old
path stays behind as a folder of its own.

Two capability flags, not one. `supported` means the core keeps a tree and its
empty folders can be drawn; `editable` means every path in that tree would also
survive moonproto's validator. They differ on a real account: MoonBot allows a
`/` inside a folder name, moonproto's own state then reports the split halves as
parent folders, and its validator refuses those — so on such a core every folder
edit would be refused whole. The terminal asks once and keeps its local marks
instead of failing per command.

An empty folder draws neither a caret that opens onto nothing nor a checkbox
that covers no strategy, and its tooltip says whether the core keeps it.

Known gaps, deliberate: a refused reorder or folder edit reaches only the log,
so the arrangement on screen reverts without saying why; the analytics purge
still deletes an emptied folder the legacy way; empty folders are hidden while a
search is running; their order among themselves is by name, which the protocol
leaves unsynchronized and MoonBot orders differently.
Both handlers — the Rust panic hook and the native SEH filter — captured a
backtrace first and wrote the record afterwards. Symbolizing goes through
dbghelp, which is not thread-safe and cannot run on an exhausted stack, so when
the capture itself faulted the process died inside the handler with nothing
written at all: an empty panic.log, an empty app log, and a Windows crash record
naming dbghelp.dll as the faulting module rather than anything of ours.

That is not a hypothetical. A startup stack overflow presented exactly this way
and was undiagnosable until the order was reversed; with the record written
first, the same run reported `NATIVE CRASH: code=0xC00000FD` and named its
address, which is the whole of what was needed.

The backtrace is still attempted, appended as a second write — panic_log opens
for append, so a crash between the two costs the backtrace and nothing else.
rust-i18n builds its backend once, lazily, on the first t!() — and its generated
initializer materialises all ~2600 keys in ONE stack frame. Whether that frame
fits depends entirely on where the first lookup happens, which nothing was
choosing deliberately: it landed deep inside chart-tab construction, the
__chkstk probe entering the frame faulted, and the application died at startup
with 0xC00000FD before opening a window.

The threshold moved because the dictionary grew — this branch adds thirteen keys
and #441 added the field-label table — but the trigger is the call site, not the
count, so a dictionary this size will cross it again from anywhere.

Touched at the base of the stack instead, the frame is built where there is room
and every later t!() is a map lookup. Verified live: the same configuration that
crashed on every launch now starts.
@guyverino
guyverino force-pushed the feat/strategy-order-and-folders branch from 67ea65b to 939e7ce Compare September 6, 2026 20:04
@guyverino
guyverino merged commit 0f2dbcc into main Sep 6, 2026
6 checks passed
@guyverino
guyverino deleted the feat/strategy-order-and-folders branch September 6, 2026 20:18
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.

1 participant