Skip to content

Version 0.1.8 - #8

Open
lnd3 wants to merge 229 commits into
mainfrom
version-0.1.8
Open

Version 0.1.8#8
lnd3 wants to merge 229 commits into
mainfrom
version-0.1.8

Conversation

@lnd3

@lnd3 lnd3 commented Jun 26, 2025

Copy link
Copy Markdown
Owner

No description provided.

@lnd3 lnd3 self-assigned this Jun 26, 2025
linuscu added 28 commits June 26, 2025 23:28
…h central char input dispatching with uiwindow.
linuscu and others added 30 commits March 24, 2026 08:38
Groups are now draggable via their bounding rectangle. Clicking in the
group rect (but not on a node) activates drag mode, moving all member
nodes together. UIData positions are persisted on mouse release.
Drag is inserted between node-move and canvas-pan in the input chain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r expand

- Group drag: update UIData every frame (not just on release) so the bounding
  rect re-computes each frame and visually follows the nodes during drag.
- Group creation from picker: cache selected node IDs in mPopupSelectedIds on
  IsWindowAppearing() instead of querying GetSelectedNodeIds each frame.
  UISelect clears selection on any left-click while the canvas is hovered
  (AllowWhenBlockedByPopup), so the fresh query on the 'Create' click frame
  returns empty — the cached snapshot avoids the race.
- Node picker tree: replace ImGui::TreeNode with ImGui::BeginMenu so categories
  expand on mouse hover, consistent with the groups submenu behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two uses of the old local 'selectedIds' variable in the Groups submenu
('Set selection as members') were not updated when the variable was renamed
to mPopupSelectedIds in the group-creation race fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve as Module

- Remove the "Groups" management submenu from the right-click node picker popup.
  Groups are organizational annotations, not instantiable node types — listing
  them in the picker was misleading and implied they could be placed as nodes.

- Right-click on a group rect now opens a dedicated group context menu with:
  Rename, Set Selection as Members, Save as Module..., Delete Group.
  Right-click on empty canvas shows the normal node picker (unchanged, with
  "Group Selection..." entry when nodes are selected).

- Add SetSaveGroupAsModuleCallback(fn) to UINodeEditor so the host (e.g.
  NodeGraphEditorWindow) can handle the actual module file save without the
  editor depending on file-system code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…de graph

Adds a lightweight per-evaluation context struct (NodeGraphContext) that
the host (SchemaRunner, ProcessNGSchemas, replay) injects once before a
ProcessSubGraph pass. Nodes read it via HasContext()/GetContext() without
requiring wired inputs or inheritance changes.

NodeGraphContext fields:
  now          — sub-candle timestamp (tick/bar/replay time)
  intervalSecs — current TF bar duration in seconds
  isBacktest   — true when running in ToolBackTester

NodeGraphBase gets public SetContext/HasContext/GetContext forwarding
methods (declared in class, defined out-of-class after NodeGraphOp is
fully declared to avoid incomplete-type errors).

NodeGraphOp stores NodeGraphContext* mContext = nullptr (null = no
context, existing nodes unaffected).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signal nodes with sub-bar replay support check ctx->subBarK to determine
how many interpolated sub-steps to run per bar. 0 = bar-level only (default).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Sha256.h: lightweight header-only SHA256+HMAC implementation
- Add Sha256ComparisonTest.cpp: verify custom impl matches CryptoPP on NIST vectors, RFC 4868 HMAC vectors, and stress tests
- Both implementations cryptographically equivalent
- CryptoPP used for verification, custom impl for low-dependency use cases

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Bug 1: Integer division precision loss in timeout calculation (line 222)
- Changed: static_cast<int32_t>(...) / 1000 to keep full int64_t precision
- Impact: Timeout calculations now accurate for millisecond ranges

Bug 2: Missing null buffer validation in WSWrite (line 297)
- Added: Check for buffer == nullptr or size == 0 before curl_ws_send
- Impact: Prevents undefined behavior when called with invalid inputs

Bug 3: Race condition in NotifyCompleteRequest (line 206)
- Removed: Non-atomic read of mOngoingRequest before atomic CAS
- Changed: Trust only the atomic compare_exchange_strong operation
- Impact: Eliminates TOCTOU (time-of-check-time-of-use) race condition

Bug 4: Incomplete error handling on curl_easy_init failure (line 81-85)
- Added: Immediate failure return and state cleanup if init fails
- Added: Error logging for diagnostics
- Impact: Prevents leaving mCurl in undefined state on init failure

Also fixed related precision issue in SetRunningTimeout (line 235) where
int32_t cast happened before addition, potentially losing precision.
NetworkConnection/ConnectionBase:
  SendAndUnReserveRequest gains postBody + postHeaders params
  When postBody non-empty: sets CURLOPT_POST, CURLOPT_POSTFIELDS,
  CURLOPT_POSTFIELDSIZE, Content-Type: application/json header
  curl_slist freed after perform. GET path unchanged (CURLOPT_HTTPGET).

NetworkManager::PostQuery: postBody + postHeaders params forwarded
  through lambda capture to SendAndUnReserveRequest.

NetworkInterface::SendJsonRequest(interfaceName, endpointPath, jsonBody, ...):
  Registers endpoint on first use (AddEndpoint is idempotent).
  Calls PostQuery with the JSON body as postBody.
  Returns result of job queue submission.

Backward compatible — all existing GET callers pass no postBody and
continue working unchanged. POST is purely additive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After NewNode() creates a node, look up its registered type name from
mRegisteredNodeTypes and call SetTypeName() on the operation. This makes
GetTypeName() usable for param matching by display name (e.g. "Level
Position Sizer 2") rather than requiring the internal C++ constructor
name. Enables --param "Level Position Sizer 2.Signal Thresh=..." to work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pagation

NodeGraphBase: add pure virtual SetTypeName(string_view).
NodeGraphOp: implement SetTypeName writing to mTypeName.
NodeGraph<T>: override SetTypeName to delegate to mOperation.
NodeGraphSchema::NewNode: call node->SetTypeName(registeredName) after
creation so GetTypeName() returns the UI-facing registered name. This
enables --param "Level Position Sizer 2.Signal Thresh=..." to match
correctly instead of requiring the internal C++ constructor name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
std::max/std::min are broken on MSVC when windows.h defines them as macros.
Use l::math::max2(), l::math::min2(), l::math::clamp() throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All TEST_EQ/TEST_FUZZY/TEST_TRUE/TEST_FALSE macros require a trailing
message string. Pass "" when no specific message is needed. Omitting it
is a compile error on MSVC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BeginArray(name)/EndArray() as named aliases for Begin(name,true)/End(true).
Unnamed BeginArray() and Begin() for positional items inside arrays.
AddBool(name, value) emits literal true/false (not 0/1 from AddNumber).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
failed_tests accumulated across all groups, making per-group successful
counts wrong and causing underflow for groups with few tests but many
prior failures. Now tracked per-group with total_failed accumulator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…::LoadJson

Previously LoadJson returned {true, 0} on success, so callers received
consumed=0 and only advanced the parse buffer by 1 byte per message.
This caused JSMN_ERROR_INVAL on every subsequent tick because the next
parse started at byte 1 (missing the opening '{').

Return mTokens[0].end instead — jsmn sets this to the exclusive end
position of the root token, which is the correct number of bytes consumed
by the first JSON object in the buffer.

Drive-by fix discovered while debugging Binance WS stream parsing in
TradeFlow ToolLiveTrader (see TradeFlow plan/actions/A015 Phase 4).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPtZK2cMJDnfgc4YsnUJnc
…n limit

Adds JsonParserDynamic alongside JsonParser<N>. Uses two jsmn_parse passes:
  pass 1 — NULL token buffer to count tokens needed (JSMN_ERROR_PART still
            propagates so partial JSON is detected correctly)
  pass 2 — allocate std::vector<jsmntok_t> of exact size, parse for real

No template parameter to tune. Same GetRoot()/LoadJson() interface.
Reuses the vector across calls so steady-state has no per-call allocation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pass

The two-pass (NULL-token count + real parse) approach is unreliable with this
JSMN build: JSMN's internal state machine behaves differently with NULL tokens
(toksuper/toknext never updated), so the token count from pass 1 can diverge
from what pass 2 actually needs, causing JSMN_ERROR_NOMEM on the second call.

Replace with grow-and-retry: attempt jsmn_parse with current vector capacity,
double on JSMN_ERROR_NOMEM, loop until success. The vector reaches its
high-water mark after the first large message and is reused with no further
allocation in steady state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When WSWrite returns an error (e.g. CURLE_SEND_ERROR while curl is
mid-receive), continuing to retry the same message up to maxQueued
times just floods the log. Break immediately — the message stays in
queue for the next tick.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
curl_multi_add_handle does not wake the curl_multi_poll loop. If the
performer thread is waiting in curl_multi_poll(1000ms), a new WS
connection sits unprocessed for up to 1 second. During that window,
IsConnected() already returns true (IsAlive checks mOngoingRequest,
not WS readiness), so the caller attempts curl_ws_send before the
HTTP upgrade is complete — causing CURLE_SEND_ERROR (-55) every tick.

Fix: call curl_multi_wakeup() immediately after curl_multi_add_handle
so the performer processes the new handle right away.

Also reduce curl_multi_poll timeout from 1000ms to 10ms so subsequent
cycles are responsive to socket events without needing explicit wakeups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
…emature curl_ws_recv

IsAlive() returned true as soon as mOngoingRequest was set (before the background
job thread even called curl_easy_perform). This let the main thread call WSRead()
→ curl_ws_recv() while the job thread was still inside curl_easy_perform doing the
HTTP upgrade, causing curl internal hash corruption and a hang/crash.

Add mWebSocketHandshakeDone (atomic_bool). Reset to false in TryReservingRequest()
so reconnects start clean. Set to true immediately after curl_easy_perform returns
(non-multiplex path) — at that point the WS is truly ready for send/recv.
IsAlive() for WS connections now requires the flag, so IsConnected() stays false
until the handshake is done. The main thread no longer calls curl_ws_recv on a
partially-connected handle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
… clear

HasExpired() had a side-effect (mTimeout = -1) inside a predicate, making it
non-const and surprising. Move that assignment to where mWebSocketHandshakeDone
is set — same point in time, cleaner ownership. HasExpired() and IsWebSocket()
are now both const.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
When recvLeft > 0 the loop always hit 'continue' before the buffer-full
guard, so if the caller buffer filled (recvMax → 0) curl was called with
size=0, returned CURLE_OK/recv=0/bytesleft>0, and looped forever. Move
the recvMax<10 check above the continue so we return -103 before spinning.
Also remove the dead 'recvLeft > recvMax' branch (only reachable when
recvLeft == 0, so the condition can never be true).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
<10 was arbitrary — any remaining space is usable capacity. Only stop
looping when the buffer is genuinely full (recvMax == 0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
The previous guard was inside the CURLE_OK handler (after the call), so
on the iteration following a full-buffer read we still called curl_ws_recv
with buflen=0 before the guard fired. Move the check to the top of the
loop so curl is never called with a zero-length buffer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
…oads

When a server-side PING, PONG, or CLOSE frame arrives between data frames,
curl delivers it with meta->flags set accordingly but still writes bytes into
the caller's buffer. Previously readTotal was incremented for all frames, so
control frame payloads (e.g. the 0xE8 byte from a close/compressed frame)
were concatenated into the JSON stream, corrupting the next parse.

Now only non-control frames (TEXT/BINARY) contribute to readTotal. Control
frame bytes are received into the buffer but not counted, so the next data
frame write overwrites them. Demote PING/PONG/CLOSE log lines to Debug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
…message

When recvMax==0 fires after valid data has already been written (readTotal>0),
returning -103 caused the caller to break without updating its fill counter,
orphaning the written bytes. Next tick the same buffer space was overwritten,
leaving stale bytes in the stream. Return readTotal instead so the caller
accounts for the bytes and the partial message accumulates correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122sSBN4GtVf2aeXgYePdMd
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.

2 participants