> For the complete documentation index, see [llms.txt](https://docs.openalgo.in/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openalgo.in/change-log/release/version-2.0.2.2-released.md).

# Version 2.0.2.2 Released

**Date: 29th August 2026**

**Stability and Security Release: closes the eventlet boundary defects behind the long-standing "the first order works and the next one hangs the app" reports, sweeps broker credentials out of every log surface across 30 plugins, stops Dhan symbol mapping routing an order to the wrong instrument, and opens the charting terminal to user-written indicators loaded at runtime with no build step. Also modernises the Motilal Oswal plugin, extends GTT to Angel One, Fyers and Upstox, gives Flow's order fields payload-driven values, and cuts a fresh install's clone from 276 MB to 20 MB**

This release spans 143 commits since 2.0.2.1 and is dominated by two sweeps that touch the whole tree rather than one surface.

The first is the **eventlet boundary**. Production runs `gunicorn --worker-class eventlet -w 1`, which monkey-patches the stdlib before the app is imported, so `threading.Lock`, `Event` and `Queue` all become green primitives owned by the hub. A handful of threads in this project are genuinely real OS threads, principally the asyncio loop in the websocket client, and every crossing between the two worlds was a defect: a real thread touching a green lock raises `greenlet.error: Cannot switch to a different thread` inside `fire_timers` and blocks that thread forever, a greenlet waiting on a C-served timeout freezes the entire worker, and a waiter that is simply never woken sits out its whole timeout on data that was ready all along. All three were present. None of them reproduce on `uv run app.py`, which is why they survived three separate issue reports (#1402, #1473, #1569) over months.

The second is **credential exposure in logs**. A grep for credential variable names finds the easy half. The larger class is a secret riding inside something else that gets logged: a WebSocket URL with the token in the query string, an auth request or response body, a headers dict, an exception message whose text embeds the full URL. Roughly twenty broker adapters were also actively replacing a redacting logger with a bare `logging.getLogger()`. Every candidate here was rendered with a realistic value and run through the production `SENSITIVE_PATTERNS` rather than read, which is what separated the 2 real leaks from the 5 sites already safe.

Outside those, the charting terminal gained runtime-loadable user indicators and moved from `openalgo-charts` 1.6.0 to 1.8.2, Flow had its 61 node types audited against the executor, the Dhan and Motilal Oswal plugins were substantially repaired, GTT reached three more brokers, and the repository stopped committing 165 MB of reproducible compression artifacts.

***

**Highlights**

* **The eventlet boundary, in four commits (`27799885`, `9a488cae`, `05496d8b`, `ebf03340`, #1402, #1473, #1569)** - what users reported was the first order going through and the next one hanging the app for over a minute, with the order itself having taken 62 milliseconds. Four separate defects, each fatal on its own.

  **A lock shared with the loop thread must be real.** The websocket client runs its asyncio loop on a real OS thread, because asyncio cannot run on a green one, and it invoked every registered market-data, auth and error callback from that thread. Four crossings existed: `WebSocketClient.lock`, the sandbox engine's `_lock`, the scalping risk monitor's `_lock` and the Flow executor's tick `data_event`. Contended, the hub tries to resume a waiter belonging to another thread and leaves the loop thread blocked on that lock forever, so the feed stops answering pings and stops resolving subscribe acks. They now take their primitives from `utils/real_threading`, which resolves to the unpatched originals under eventlet and the stdlib otherwise. The `Event` needed more than a swap: a green `Event` never wakes on a `set()` from a real thread, and a real `Event.wait()` wakes but blocks the hub, so `real_threading.wait_for` polls the flag and yields between checks.

  **Logging handler locks and inline callback dispatch.** `logging.Handler` builds its lock in `__init__`, which under gunicorn happens after monkey-patching, so it is green, and every real thread in this project logs. Measured, a real thread logging while a greenlet holds the handler lock deadlocks permanently and raises exactly the production traceback; #1569 reports the give-away that precedes it, `AttributeError: 'StreamHandler' object has no attribute 'lock'` on unrelated requests. `Handler.createLock` is now patched on the class, so third-party handlers get a real lock too. Separately, `_handle_message` ran on the loop's real thread and called every subscriber inline, and those subscribers reach SocketIO, the event bus and the sandbox engine. The loop thread now only enqueues onto a real bounded queue and a green thread does the calling.

  **`PRAGMA busy_timeout` waits inside C.** A greenlet waiting that way freezes the entire worker for the whole timeout, and that is not merely slow: the greenlet holding the write lock cannot be scheduled to commit while the waiter is blocking the hub, so the wait can only ever end in "database is locked". Measured, a holder that needed the lock for 0.5s produced a 16.46s failure; waiting the same 0.5s in Python succeeds in 0.47s with the hub running. The in-SQLite wait drops to 100ms and the real waiting moves into a Python retry loop, keeping the same 15-second total budget.

  **`run_coroutine_threadsafe` never wakes its caller.** It hands back a `concurrent.futures.Future` whose `result()` waits on a green `threading.Condition` while the loop thread resolving it is real, so the future resolves correctly and the caller sleeps on regardless. Measured, an ack that arrived in 0.3s cost the caller the full 10s, which is why `subscribe()` took its whole 12 seconds whenever it had to wait at all. Nothing crosses the boundary now except one plain boolean.

  `test/test_eventlet_cross_thread_locks.py` and `test/test_sqlite_lock_cooperative.py` prove each direction under a real hub, in a subprocess because `monkey_patch()` cannot be undone, asserting on elapsed time and hub liveness rather than return values, which were always right. `95d6d1fa` records the whole rule set in CLAUDE.md so the next person does not rediscover it.
* **Broker credentials no longer reach the logs (`209c305e`, `438af07f`, `c8dad476`, `fe0875f3`, `4c3abf7f`, #1854, #1855)** - four passes, each finding a class the previous one could not see.

  **73 call sites across 60 files in 30 plugins** were using a bare `logging.getLogger()`, which bypasses `SensitiveDataFilter`. This was not merely a missing filter: `websocket_proxy/base_adapter.py` already sets a redacting logger, so every adapter subclass reassigning `self.logger` in its own `__init__` was actively replacing it with a non-redacting one.

  **Credentials interpolated directly into messages**, across 12 plugins. Removed entirely rather than truncated, because a 10-character API key prefix is still key material and fivepaisa logged the first 50 and last 50 characters of its access token on adjacent lines, which together expose far more of a JWT than either alone.

  **Secrets riding inside a URL, payload, headers dict or exception message**: 35 sites across 24 files in 15 plugins. The worst were flattrade logging `hash_input`, which is literally the API key concatenated with the secret; definedge putting the API key in the URL *path* and then logging the `httpx.HTTPStatusError` whose message embeds the full URL, so it leaked on every 4xx, which is exactly the wrong-credential case; aliceblue logging the full login response at INFO on every login; and upstox logging the AWS SigV4 presigned feed URL including `X-Amz-Signature`.

  **A defect in the redaction pattern itself.** `(Bearer\s+)[\w\-\.]+` stops at the first separator, and two brokers send a two-part credential in that header, so the filter redacted the harmless half and left the real secret. Found independently in tradejini and aliceblue, which is what proved the pattern was wrong rather than the call sites. `cookie` was added to the alternation, `utils/logging.py` gained its first tests, and the frontend stopped sending mutating requests with no CSRF token when `fetchCSRFToken()` throws.
* **Dhan: symbol mapping routed orders to the wrong instrument (`da553a56`, `49717118`, #1929, #1930, #1932, #1934, #1924)** - four related defects in the master contract and the feed, all verified against the live 197,254-row scrip master.

  Dhan scopes `SEM_SMST_SECURITY_ID` per segment, not per exchange, and `assign_values` matched on instrument name alone. NSE segment D and segment M both mapped to NFO, giving 8,642 security ids two contracts each: token 153964 was both a TCS option and a SILVERM option, and `get_symbol()` returns the first match. A live equity option position could therefore resolve to a commodity symbol, the position book lookup would miss it, and a strategy would conclude it was flat and skip the exit. Colliding rows go from 17,284 to 0, NSE segment M now maps to NCO with 25,400 rows, and 722 stranded MCX index option rows are recovered.

  On the forward lookup, symbols were built from display strings. The equity branch ignored `SEM_SERIES`, so ELECTCAST was both the equity and its warrant and an order could reach the wrong one; strikes were parsed from the two-decimal `SEM_CUSTOM_SYMBOL`, which misformatted 1,628 NFO contracts as `.50CE` and, in currency, collapsed EURUSD 1.010 and 1.015 onto one symbol. **Breaking: 7,190 NSE equity symbols gain a series suffix** (`ELECTCAST-W1`, matching Zerodha's convention). Only 9 were genuinely ambiguous and the rest are mostly bonds and government securities, but the BE, SM and ST series include tradable equities, so a strategy referencing one by its bare symbol needs updating. No migration is required, since `master_contract_download()` clears the table before reinserting.

  `unsubscribe()` only forgot the instrument locally and never told the broker, so subscriptions accumulated toward the 5,000-per-connection limit invisibly, Dhan not acknowledging subscribes. `dhan_sandbox` was worse: it sent `{"RequestCode": 0}`, which is not a valid code at all. Both now send the code matching the mode the token was subscribed in. `dhan_sandbox` also carried byte-identical copies of every mapping bug above and is brought to parity.
* **User chart indicators, loaded at runtime (`f4359718`, `a95460fe`, #1923)** - drop a `.js` file in `strategies/indicators/` and it becomes an indicator in the `/trading` picker, with a generated settings dialog, legend row and saved-layout persistence. No build step, no Node.js, no restart. They are loaded over HTTP rather than bundled, because `frontend/dist/` is built by CI from what is committed and user indicators are deliberately gitignored, so a bundled one would be erased by the next `git pull`. The folder sits under `strategies/` to mirror `strategies/scripts/`, which also puts it inside the `openalgo_strategies` Docker volume. Custom indicators register after the built-in tier, so a colliding id overrides a built-in. Validation happens in the browser against the library the chart already has loaded, catching both silent killers: a column that is not `bars.length` long, and a plot key `calc` never filled. Neither raises anything at runtime, which is what made a broken indicator look like one that simply does not work.
* **Charting terminal: 1.6.0 to 1.8.2, and everything the pin was hiding (`8eda625d`, `cbacb83b`, `0dc8ac19`, `a1a2dc92`)** - the pin sat at 1.6.0, so none of the descriptor surface added since was reachable. Bumping it exposed a chain of gates that would each have rejected a valid indicator before the user saw it: the loader accepted six input types where the library now defines eleven, the settings dialog could render none of the five new ones, and the column check assumed one column per plot so a candle plot fed by an `ohlc` group read as four missing columns. Separately, the chart was never told the instrument tick size, so its price scale inferred precision from the visible range and RELIANCE rendered as 1,303.5 rather than 1303.50 while drawings snapped to a grid nobody chose. The terminal already had the number from the symbol master and simply never passed it. The version is now pinned exactly rather than with a caret: `^1.6.0` resolves to 1.9.0 on the registry, and deprecation only prints a warning, it does not stop an install.
* **Charting terminal features (`5ea4d080`, `d7cdbf54`, `c0d73cee`, `55447bd0`, `7b867c9d`, `0c8c29a6`)** - market replay with a transport bar and scrub; a chart settings dialog rendered from the engine's own declarative schema across five tabs, with a reset that resets to *this terminal's* baseline rather than the engine's per-control defaults; chart sync groups linking crosshair, time and symbol independently; a warm-load history cache, measured at two history requests for a cold symbol and one for a return visit; toolbar undo and redo for drawings; and an indicator browser with categories, favourites and recents replacing a single 91-entry scrolling column. Plus the corner session clock reading the exchange's wall time through the chart's configured timezone.
* **Motilal Oswal: repaired and modernised (`f76818be`, #1912)** - every endpoint moved to its documented version (placeorder v1 to v2, modifyorder v2 to v5, getorderbook v2 to v5, gettradebook v1 to v4, getposition v1 to v4, getdpholding v1 to v3). `get_open_position` matched a broker scripname against a position-book symbol and never matched, so every smart order saw a flat position; smart orders also looked up positions with no exchange, so F\&O MIS positions never matched. The env convention was corrected: sending the secret as `ApiKey` caused MO2005, and the client code is now persisted from the TOTP page rather than read from an env var, fixing the empty vendorinfo (MO2012). Then a WebSocket audit found four leaks: the services build a fresh `BrokerData` per request so an instance-cached socket could never hit, and every depth and multiquote call opened a brand new WebSocket that nothing closed. Pooled per session in a module-level registry, five requests now open one socket instead of five. Three further leaks surfaced on re-audit: a cold-start registration race (4 concurrent requests created 4 sockets and leaked 3), duplicate poll threads on every proxy recovery path republishing every subscription to ZMQ, and tick caches never purged, which an option-chain sweep grows by hundreds of strikes per refresh. Pinned by eight tests mirroring `test_aliceblue_ws_reuse.py`.
* **GTT for Angel One, Fyers and Upstox (`6d7f2552`, #1922)** - extends GTT beyond Zerodha and Dhan. Each broker registers itself by shipping `api/gtt_api.py` plus `mapping/gtt_data.py`, with the compatibility matrix derived by parsing for it. Angel One has no OCO rule type, so an OpenAlgo OCO becomes two rules returned as a composite trigger id, with the first leg rolled back if the second fails. Fyers signals OCO by the presence of `orderInfo.leg2` and requires leg1 above the LTP, so the target leg maps to leg1. Upstox has no OCO at all: every GTT needs an ENTRY rule, making its multi-leg form a bracket rather than an exit-only pair, so an OpenAlgo OCO opens the entry position before arming the legs, logged loudly at place time because the resulting EDIS rejection names demat authorisation rather than the entry leg.
* **Flow: nodes no longer act on data they do not have (`cb954658`, `ce9db84a`, `44ebce13`, `732afe4d`, `98e693db`, `252bc7c1`, #1904, #1909)** - an audit of all 61 node types, each finding reproduced before it was touched. `priceCondition`, `positionCheck` and `fundCheck` read the broker response without checking its status, so a 401 gave LTP 0.0 and `status: success`: "if LTP < 100 then BUY" fired on an expired session and "if no position then BUY" doubled an open one. An errored condition settled a gate with its placeholder `False`, driving a real order down the FALSE branch before the run was marked failed. A condition reachable by two paths ran twice and placed two orders from one trigger. `timeWindow` could not cross midnight, making 22:00-02:00 unsatisfiable and, inverted, always true. Subscriptions were never given back to the process-wide websocket singleton, and a specific-mode `unsubscribe` with no symbol fell through to `unsubscribe_all`, clearing the subscriptions the sandbox engine uses to trigger pending SL and LIMIT orders. Two injection paths in `httpRequest`: the URL was interpolated twice so a payload could substitute its own `{{...}}`, and header JSON was interpolated before parsing so a value carrying a quote became structure.

  Separately, every order field now accepts a `{{reference}}`, not just `symbol`: a dropdown cannot express `{{webhook.exchange}}` and a number input cannot express `{{webhook.quantity}}`, so a webhook-driven order could name its instrument and nothing else. The executor always interpolated these; the gap was entirely in the form. Interval schedules are anchored to the clock rather than to activation, so a 5-minute job lands on :00, :05, :10 rather than five minutes after a button press, offset by `FLOW_INTERVAL_ALIGN_OFFSET` (2s) so the run sits just inside the new bar. Derivative segments default to NRML rather than a stored MIS on every node. MCX commodity options are resolvable, priced off the nearest unexpired future since there is no spot instrument, and multi-leg baskets can be built leg by leg instead of only through hand-written JSON.
* **Shoonya returns a quote for a different instrument, 9% of the time (`252bc7c1`, part of #1904)** - measured against the live API, two NFO options polled round-robin at 2 req/sec for 13 minutes: 119 of 1318 replies came back as a complete, `stat=Ok` snapshot of another instrument, every one of them the NSE index, in a run that never requested the index. Nothing about the price reveals it: a leaked index quote is internally consistent and its LTP sits inside its own OHLC, so the stale-quote check cannot catch it. It reaches order fills, and a sandbox MARKET buy on `NIFTY18AUG2624600CE` filled at 24391.25, the NIFTY spot, against an LTP of 39.85. All five `GetQuotes` paths now compare the echoed exch/token against the request and retry up to three times; the two multiquotes fetchers bypassed `get_api_response` entirely and had no check at all. The same session's WebSocket feed carried 1719 ticks with zero wrong instruments, so this is specific to the REST endpoint.
* **Repository size: 276 MB to 20 MB per install (`4fe47bd0`, `02361622`, `a557c12e`, #1896, #1897, #1898)** - the Vite build emitted `.br` and `.gz` next to each hashed asset and CI force-committed them on every rebuild. Compressed output can be neither deflated nor delta-compressed by git, and content-hashed filenames mean every rebuild produces new blobs that never go away, so these had grown into 165 MB of the 248 MB `frontend/dist` occupies in history. Repository history had tripled in 60 days. They are generated at startup now, idempotently and atomically, in about 30 ms on a warm boot. Tracked `dist` files drop from 586 to 270 and history growth from roughly 83 MB/month to 28 MB/month. Alongside it, all four install scripts and the nine user-facing clone commands in the docs use `--filter=blob:none`, measured against GitHub at 20 MB and 8s rather than 280 MB and 36s, keeping all 4,824 commits and every branch and tag. `install-multi.sh` clones inside its per-instance loop, so a five-instance install went from \~1.4 GB.
* **Sandbox: three clock boundaries resolved (`7403631b`, `5292719f`, `e4918105`, `f0b39cf1`, `c3e568ea`, #1789, #1801, #1808)** - `sandbox_positions.updated_at` is written by `func.now()`, which on SQLite is UTC, and `get_open_positions` compared it against a boundary built from naive local wall time. On an IST host the boundary landed 5:30:00 late, dropping open positions last updated between 03:00 and 08:30 IST from the position book, which is what a polling strategy reads to decide whether it is flat. The same shape affected the T+1 settlement cutoff, pushing a CNC position created between 00:00 and 05:30 IST into settlement a day early. `as_db_utc()` now exists in one place, since this file has had the same bug twice. Separately, a `SESSION_EXPIRY_TIME` of `'25:00'` parses cleanly as an int and then raises on `replace()`, outside the try, and `catch_up_mis_squareoff()` swallows it, so a config typo silently disabled a risk control. And two same-symbol fills arriving together each read the same quantity and wrote their own delta, so the second overwrote the first with both orders still showing complete; the regression test fires several concurrent pairs because a single pair only interleaves about two thirds of the time.
* **Holdings returned no LTP or average price (`5bd4cd6a`, `d36936a6`, #1917, #1919)** - Angel ignored `averageprice` and `ltp`, and Zerodha read `last_price` to derive the P\&L percentage and then threw it away. The holdings page rendered a dash in both columns, the CSV export wrote them empty, and portfolio analytics weighted by a zero investment value. Both brokers also failed the whole request on one bad row: Angel reports an empty demat as `holdings: null` rather than `[]`, so an account with nothing bought got an HTTP 500 instead of an empty table, and a single Zerodha holding with a null `average_price`, `last_price` or `pnl` turned the entire request into a 500 rather than degrading that one row.
* **Flattrade reported a funded pledge as no pledge (`3ca1adfa`, #1936)** - the Limits response carries several collateral figures and the wrong one was read. `brkcollamt` is the pre-valued collateral amount and sits at 0.00 on an ordinary pledged account, while the pledged value lives in `collateral`, which is what the Flattrade app labels "Holdings Collateral". The reported account held 9,385.34 cash and 1,890.30 of pledged NSE collateral; OpenAlgo showed the cash correctly and collateral 0.00. Shoonya and Zebu share this Noren API and carry the same line, left alone here pending confirmation against a live pledged account.
* **CORS\_ENABLED=FALSE opted the deployment&#x20;*****into*****&#x20;the permissive policy (`c88b5c8e`, #1860, #1848)** - `get_cors_config()` returned an empty dict when disabled and flask-cors then applied its own defaults, which set `origins="*"`. Measured against the previous behaviour, a GET to `/api/v1/funds` carrying `Origin: https://evil.example` returned that origin back, and an OPTIONS preflight for a JSON order POST returned 200 allowing `x-api-key`. That let a cross-origin page place an order through the user's own instance with a leaked API key, from the user's broker-registered IP. The same applied to any `.env` predating the variable. The enabled-but-unconfigured case now fails closed too.
* **TradeSmart quotes were throttled to a fraction of the allowance (`b8e4cbdd`, #1928)** - quotes are metered separately by the broker, so pacing them against the shared 10/sec and 120/min per-user budget was wrong. `/GetQuotes` now reserves from its own 100/sec bucket with no per-minute ceiling, and the REST quote fan-out pool sizes to that rather than to the general constant, which had capped throughput at 8/sec regardless. Orders, funds, margin and history keep the shared gate. Placed orders are also tagged `openalgo` in the Noren `remarks` field, which the OMS echoes back in the order book.
* **Option resolver validation (`0c41679d`, #1829)** - the strike interval was used as a divisor with no guard, so 0 raised `ZeroDivisionError` and a negative, NaN or infinite value produced a strike that formats into a symbol no exchange lists, both surfacing as HTTP 500. An unrecognised option type was silently priced as a put: every offset calculation branches on CE and takes the else branch for everything else, so "CALL", "C" or `None` resolved to the put strike with no error and a log line saying it was correct. 145 test cases; neutering the two validators fails 91 of them.
* **Auth: no way out of a half-finished login (`cab45dc9`, `51ea6013`)** - the broker-auth pages render outside `Layout` and therefore have no navbar, and `POST /auth/login` redirects back to `/broker` whenever a user is already in the session, so someone who completed the password login but not the broker login had no UI path out. The only escapes were typing `/auth/logout` by hand or clearing cookies. The confirm dialog is shown unconditionally rather than signing out on the first click, because OpenAlgo allows concurrent devices sharing one broker feed and `logout()` takes the full teardown path across every device. Password login also clears the session before writing any authenticated value, so a reset token or a half-finished TOTP park cannot survive underneath.
* **CI now tests what the project advertises (`a551845b`, #1894)** - `pyproject` declares `requires-python >=3.12` and the frontend declares three Node majors, but CI pinned one point in each range. `backend-test` now runs on Python 3.12, 3.13 and 3.14 and the frontend jobs on Node 20, 22 and 24, with `fail-fast` off. All 166 pinned dependencies resolve to an identical set on all three Python versions, so this is coverage rather than a porting effort. Note that matrix jobs report as `backend-test (3.12)`, so a required status check list must use the expanded names.
* **Test isolation was decided by import order (`4852d463`)** - `conftest` assigns `DATABASE_URL` to the test databases, then `utils/config.py` calls `load_dotenv(override=True)` at import and puts the real paths back. Whether that happened depended entirely on which module a given test imported first. The visible result was seven Flow workflows from the QA regression suite appearing in the operator's Flow Editor, named "retention test", "age test", "mine", "theirs", removable only by hand. Verified before and after: a full run put seven workflows in `db/openalgo.db` before and none after.
* **A silent feed logged a warning every two minutes (`3f2fa11e`)** - a subscribed feed is legitimately silent every night, every weekend and every trading holiday, and the check cannot tell that apart from a dead socket without an exchange-aware calendar. A Saturday afternoon logged an identical line forty times in ninety minutes, counting up to "no ticks for 4716s". Moved to debug; `get_adapter_health()` still reports seconds since the last tick per user.
* **Smaller fixes** - the frontend rate limiter retained a timestamp exactly `windowMs` old, so waiting exactly as long as the module reported was not enough (`78e4e490`, #1830). Multi-option Greeks batch state is keyed by leg index (`0f99ab6c`, #1819). Client error reports no longer persist a reset-password token or a broker OAuth code into `log/errors.jsonl` (`e9b29821`, `07a5f649`, #1851). `get_history()` rejects an unsupported source at the entry point (`c654d85e`, #1826) and the market calendar helpers return 400 rather than 500 for a non-string date (`242f80d4`, #1824). Navigation links declare `aria-current="page"`, which no link in the app did (`1d4cade3`, #1833). The dead `zmq==0.0.0` stub, a placeholder package shipping no code, is dropped from all three dependency lists (`4fa997dc`, #1895). Ten boot-time lines that state an expected fact moved from INFO to debug (`5fd89238`). The security audit's own NullPool check was failing on the documentation warning against the defect (`301fe95a`).
* **Docs and skills** - broker plugin counts synchronised to 36 across 17 files plus the FAQ, the devsprint guide and the design docs, with historical snapshots deliberately left alone (`eb7d25c2`, `122dbed5`, #1844, #1906, #1910). A documentation-only contribution workflow (`287e8d02`, #1846), a completed CONTRIBUTING table of contents (`447786fa`, #1883), contributor test commands aligned with what CI actually runs (`1d6ce13f`, `a32cb277`, #1841), corrected frontend build-artifact and Node guidance (`6d3a0d08`, #1842) and obsolete `/react` routes replaced with `/dashboard` (`7e409e49`, #1840). Five claims in the Flow import format were proved wrong by the docs rewrite and corrected (`6ed6442d`), including `marketHoursOnly` defaulting to false in the executor while the editor writes true, and `days` never being read by the `daily` branch, so a "weekdays only" daily schedule still fired at the weekend. Three new skills: `chart-indicator` (`f4359718`), `flow-builder` (`cb954658`) and `verify` (`262abe06`), the last encoding six rules each earned from a specific failure in this cycle rather than invented.

***

**Dependencies**

* `openalgo-charts`: **1.6.0** to **1.8.2** (eleven input types, `ohlc` plot groups, draws, background and bar colours, instrument tick on the calculation context, and the stop-limit drag fix that had been sending `price: 0` to the broker)
* `zmq==0.0.0` removed from `pyproject.toml`, `requirements.txt` and `requirements-nginx.txt`. It is a placeholder package on PyPI shipping no code; the `zmq` module every call site imports comes from `pyzmq`, which is already pinned.

No other Python dependencies changed. The pinned `openalgo` SDK stays at 2.0.3.

***

**Contributors**

* **@marketcalls (Rajandran)** - release management; the eventlet boundary sweep (#1402, #1473, #1569) and its regression suites; the four-pass broker credential redaction sweep and the `SensitiveDataFilter` Bearer defect; Dhan master contract, symbol construction, NCO segment and unsubscribe fixes (#1929, #1930, #1932, #1934); runtime-loaded user chart indicators (#1923); the charting terminal from `openalgo-charts` 1.6.0 to 1.8.2, replay, settings dialog, chart sync, history cache, undo/redo and the indicator browser; Flow node-contract audit across all 61 types, payload-driven order fields, clock-anchored interval schedules and the market-hours window; the sandbox session-boundary and T+1 clock fixes; Flattrade collateral (#1936); repository size work (#1896, #1897, #1898); Motilal Oswal WebSocket pooling and leak fixes; CI matrix across Python 3.12-3.14 and Node 20/22/24 (#1894); test database isolation; the `chart-indicator`, `flow-builder` and `verify` skills.
* **@Kalaiviswa** - Motilal Oswal plugin repair and modernisation (#1912); GTT for Angel One, Fyers and Upstox (#1922); Angel and Zerodha holdings LTP, average price and null-row resilience (#1917, #1919); Dhan unsubscribe and master contract PRs (#1932, #1934); TradeSmart order tagging and the 100/sec quote budget (#1928); Flow MCX commodity options, leg-by-leg multi-leg baskets and NRML defaults on derivative segments (#1904, #1909); the Shoonya wrong-instrument quote guard.
* **@santhiprakash (Santhi Prakash)** - sandbox position-book session boundary in the database clock (#1789); MIS square-off by `updated_at` session boundary (#1801); concurrent position update serialization (#1808); dropped unused `market_data` secondary indexes in Historify (#1803).
* **@siddharthg2309 (Siddharth Gouthaman)** - history source allowlist at the service entry point (#1875); client error report URL sanitization on both boundaries (#1886); `aria-current` on navigation links (#1880); option chain view mode toggle coverage (#1876).
* **@solstxce** - API key redaction in core service logs (#1914); credential removal from broker logs (#1916).
* **@nightcityblade** - client errors for invalid calendar dates (#1861); completed CONTRIBUTING table of contents (#1883).
* **@WilliamK112 (Ching Wei Kang)** - documentation-only contribution workflow (#1907); rate limiter expiry at the window boundary (#1868).
* **@ANONYMOUSZED-beep (Arun)** - `CORS_ENABLED=FALSE` honoured instead of falling through to the flask-cors wildcard (#1860).
* **@Narasimha722 (NarasimhaReddy)** - strike interval and option type validation at the option resolver boundary (#1829).
* **@Pragitics (Pragit R V)** - multi-option Greeks batch state keyed by leg index (#1885).
* **@Meraj-08 (Md Meraj Alam)** - eliminated contradictory hard-coded broker plugin counts (#1906).
* **@K-PRAGALATHAN (PRAGALATHAN K)** - converted the history format reconnaissance script into hermetic pytest coverage (#1887).
* **@NavadeepDj (NavadeepDJ)** - type hints and a 37-case suite for the data schema validators (#1864).
* **@suhaslord (Suhas)** - contributor test commands aligned with CI (#1889).
* **@thaildhe172591 (Luu Thai)** - corrected frontend build-artifact and Node guidance in CONTRIBUTING (#1863).
* **@yiheng-kkk** - replaced obsolete `/react` frontend routes in CONTRIBUTING (#1867).
* **@PadmaBalajiL (Padma Balaji Leelavinodhan)** - devsprint participants list (#1814).
* **@cracker314** - devsprint participants list (#1817).

Thank you to everyone who filed an issue, reproduced a defect or reviewed a pull request this cycle. The eventlet work in particular rests on three separate reports (#1402, #1473, #1569) filed months apart by users who had every reason to believe the problem was on their own machine.

***

**Upgrading**

`git pull` on `main` is the canonical path - `frontend/dist` is tracked there and rebuilt by CI, so no Node.js is needed on the server. The compressed `.br` and `.gz` asset variants are no longer committed and are regenerated at startup instead, so the first boot after this upgrade spends a few seconds on that and every later boot finds them current.

Run `cd upgrade && uv run migrate_all.py` on an existing installation. This release ships migration 012, which drops three unused secondary indexes on Historify's `market_data` table: none of them leads with `symbol`, so the planner never selected them, while their DuckDB ART index memory stayed fully resident and caused out-of-memory failures on large 1-minute backfills (#1779). The drop roughly halves the database file and speeds up ingest about 1.6x at no query cost. Use `--status` first if you want to see what would change.

**Dhan users:** 7,190 NSE equity symbols gain a series suffix in this release, so ELECTCAST now names the equity and ELECTCAST-W1 its warrant. The master contract clears and reloads on your next login, so no migration is needed, but a strategy referencing a non-EQ series by its bare symbol has to be updated. Most of the affected rows are bonds and government securities; the BE, SM and ST series include tradable equities, so check those.
