Changelog
Every change you can see, newest first. Additive changes never break a client that ignores unknown fields; breaking ones are announced before they ship.
- Additive
v2 trigger capabilities
New operation
GET /v2/triggers/capabilities(get_trigger_capabilities_v2,readscope; users/v2/triggersis enabled for):TriggerV2Capabilitieswithkinds(each kind's spec fields),expressions(variables, functions incl.crosses_above/crosses_below, operators, source / depth / node limits),limits(this deployment'sTRG_MAX_*) and, with?account=,account(broker_protection_enabled, the broker order types that can hold protection, and each protection factverifiedorunverified).404account_not_foundfor an account that is not the caller's. Nothing changes for existing operations. - Additive
validate a v2 trigger without creating it
New operation
POST /v2/triggers/validate(validate_trigger_v2), same body and auth ascreate_trigger_v2(triggersscope; users/v2/triggersis enabled for). Nothing is published, stored or counted against the account's order budget.200withTriggerV2Validation:valid,errors[]{field, message}(create's422messages),reads{symbols, fields, variables},limits(legs / expressions / bytes and their maxima,ok,reason),protection(per partheld_bybroker|server,level,adjusted_by_server,note; plusreason).400,403,404account_not_found,503as on create. Nothing changes for existing operations. - Additive
trigger_update pushes (stream and webhooks, opt-in)
Nothing reaches a client that did not ask for it.
/v1/stream: new client messagesubscribe({"type":"subscribe","topics":["trigger_update"]}), answered with a new server messagesubscribed(topicsgranted;trigger_updateonly for users/v2/triggersis enabled for). After it, new server messagetrigger_update(data:TriggerV2Update). Neithersubscribednortrigger_updateis ever sent to a connection that did not sendsubscribe.- Webhooks: new event
trigger_update(WebhookTriggerUpdate), sent only to receivers with the new optional fieldtrigger_updates: true(set withPATCH /v1/postbacks/{id}(WebhookPatch); a new webhook starts without it; shown onWebhookViewonly while on).trigger_updateis not accepted ineventsand never appears there. Delivered only when the server runs withOMS_TRIGGERS_V2_WEBHOOKS=on. TriggerV2Update:event_id(de-duplicate on it),trigger_id,kind,account,strategy,from,to,transition_seq,actions,tick{symbol, ltp, event_ts},reason,result(a refused command:{op, command_id, outcome, reason}),protection(the trigger's map),at. One per change; never another user's trigger.- Stored / NATS: new record kind
trg2_updateonoms.trg2.{user}.{trigger_id}(kept byOMS_EVENTS, which now also capturesoms.trg2.>); webhook records gaintrigger_updates(default false); delivery jobs may carryevent_type: "trigger_update"(readers first: seeOMS_TRIGGERS_V2_WEBHOOKS).
- Additive
v2 protection map shows the current stop-loss level
GET /v2/triggersandGET /v2/triggers/{trigger_id}: a server-heldprotection.stop_loss.levelis now the stop the engine holds now (after trailing or tightening), not the level planned at create. Same field, same type; exampleget_trigger_v2(success) re-recorded: 2475 -> 2480 after the engine moved the stop. Agreed by the user on 2026-10-12. - Additive
pause and resume v2 triggers
New operations
POST /v2/triggers/{trigger_id}/pause(pause_trigger_v2) andPOST /v2/triggers/{trigger_id}/resume(resume_trigger_v2): hand the engine's pause / resume command for the caller's live trigger,202with the sameTriggerV2Acceptedbody as cancel;404trigger_not_found,409trigger_fired,403,429,503as on cancel. Only for users/v2/triggersis enabled for (OMS_TRIGGERS_V2_USERS); API keys need thetriggersscope. Nothing changes for existing operations. - Additive
trigger engine v2 shadow mode records (off by default)
Shadow mode (spec §12 step 2,
OMS_TRIGGERS_V2_SHADOW=on) adds its own names, none shared with the live engine and none read by anything that places orders: subjects{ENV}.trg.shadow.cmd|evt|res.{bucket}.{user}.{trigger_id}and{ENV}.trg.shadow.ctl.released, streams{ENV}_TRG_SHADOW_CMD/{ENV}_TRG_SHADOW_EVT, KV{ENV}_TRG_SHADOW_STATE, Redisoms:triggers:{ENV}:shadow:members|serves, leasesoms:lease:trg:{ENV}:shadow:{bucket}andoms:lease:trg:{ENV}:shadow:singleton, tick-gate keys{symbol}.shadow-{bucket}in the shared{ENV}_TRG_SYMBOLS. New record kinds:trg_shadow_event(the shadow book's transition: thetrg_eventfields withevent_idnamedshadow_event_idand amode: "shadow"marker, so it never reads as an event; fixtures intrg2_shadow_link(Redis hashoms:trg2:{ENV}:shadow:legacy, shadow trigger id → JSON{v, legacy_id, user, account, symbol, at}) andtrg2_shadow_diff(Mongotrg2_shadow_diffs: per trigger per IST day{_id, kind: "trigger", date, legacy_id, shadow_id, user, symbol, v2[], python[], findings[], counts, updated_at}and per day{_id, kind: "summary", date, counts, known, triggers, skipped, updated_at}), fixtures commands reusetrg_command. LegacyCIRRUS:TRIGGERS:*records are only read. Record kinds introduced in this release; no REST, webhook or stream shape changes. - Additive
trigger engine v2 Redis keys carry the environment
Every Redis key of trigger engine v2 now names its environment (
OMS_TRIGGERS_V2_ENV, escaped withdomain::subject), as its subjects already do, so two environments sharing one Redis never take each other's buckets or read each other's records: membershipoms:triggers:{ENV}:members/oms:triggers:{ENV}:serves, leasesoms:lease:trg:{ENV}:{bucket}/oms:epoch:trg:{ENV}:{bucket}, the read modeloms:trg2:{ENV}:{id}/oms:trg2:{ENV}:user:{user}(stored kindtrg2_record, hash fields unchanged) and the exit loop's claimsoms:trg2:{ENV}:evt:{event_id}/oms:trg2:{ENV}:evt_lease:{event_id}. This replaces the unprefixed names in the entries below; no release ever wrote or read them (record kinds introduced in this release; the twotrg2_recordfixtures only had their description of the key updated). The per-position exit guardsoms:trg2:position_lock:{position}/oms:trg2:position_exits:{position}stay environment-free on purpose: they guard one real broker position, whichever environment exits it. Protection:oms:trg2:{ENV}:protection:{id}(trg2_protection, fixture descriptions only),oms:trg2:{ENV}:protection:live,oms:trg2:{ENV}:protection_want:{id},oms:trg2:{ENV}:protection_lock:{id},oms:trg2:{ENV}:protection_watch; the guardoms:trg2:protection_guard:{position}stays environment-free (one real broker position). The engine's own keys (oms:engine:*, account leases) are unchanged./v2/triggersreads now answer 503 (as writes already did) on a pod withoutOMS_TRIGGERS_V2_ENV. No REST, webhook or stream shape changes. - Additive
trigger engine v2 fixtures finalized before first write
Record kinds introduced in this release; fixtures finalized before first write. The trigger engine v2 kinds (
trg_command,trg_snapshot,trg_event,trg_result,tick_snapshot,trg_bucket_mark,trg2_record) are hand-written: no build onmainwrites or reads them (checked 2026-10-12 againstorigin/main2296793: notrigger-engine/trigger-runtimecrate, notrg.*subject, noTRG_*stream or bucket, nooms:trg2:*key). So the threetrg_commandfixtures were edited in place (milestone 2 added the requiredscope) rather than appended to, whichCLAUDE.mdotherwise forbids because old records exist in production; these never did. Againstmainevery one of these fixture files is an addition and no existing fixture changed. From the first production write on, they are append-only like every other kind. - Additive
trigger engine v2 command results
New stored / NATS record kind
trg_result: the engine's answer to each command,{v, cmd_id, trigger_id, scope, op, cmd_seq, outcome (applied | duplicate | rejected), reason?, state?, transition_seq, at, epoch}, published on{ENV}.trg.res.{bucket}.{user}.{trigger_id}into{ENV}_TRG_EVT(which now also captures{ENV}.trg.res.>) withNats-Msg-Id = cmd_id, after the command's effect is durable. Fixtures and The projector applies them: the read-model recordtrg2_recordgains optionalresult_seqandlast_result(new fixturehandwritten-2026-10-12-rejected-by-engine.json), and a create the engine refused now readsstate: "rejected"with itsreasonin/v2/triggers(stateis documented as an open set;/v2/triggershas not shipped). Record kind introduced in this release; no reader or writer existed before. - Additive
trigger engine v2 subjects carry the environment
Every trigger engine v2 subject now starts with
{ENV}., as the tick feed's{ENV}.ticks.>does, so two environments on one NATS account never capture each other's messages: commands{ENV}.trg.cmd.{bucket}.{user}.{trigger_id}(stream{ENV}_TRG_CMD), events{ENV}.trg.evt.{bucket}.{user}.{trigger_id}(stream{ENV}_TRG_EVT), the release notice{ENV}.trg.ctl.releasedand the (unbuilt) risk shape{ENV}.risk.agg.{strategy}.{user}. This replaces the unprefixed names in the 2026-10-09 entries below; no release ever wrote or read those subjects (record kinds introduced in this release), and the payloads are unchanged. The runtime refuses to start on an existing{ENV}_TRG_CMD/{ENV}_TRG_EVTthat does not capture the new subjects. The api and the runtime read the environment from one setting,OMS_TRIGGERS_V2_ENV(aliasTRG_ENV). - Additive
/v2/triggers protection map
get_trigger_v2(GET /v2/triggers/{trigger_id}) andlist_triggers_v2(GET /v2/triggers) answer a new fieldprotectionon every trigger: what protects the position, part by part, keyedstop_loss,stop_condition,target,target_1…,target_condition,trail(schemaTriggerV2ProtectionPart:held_bybroker|server,broker_order_id,level,adjusted_by_server,statusprotected|degraded|unknown|held_by_server|ended,note). It is null for alerts and risk limits and for triggers created before this release.modify_trigger_v2refuses (422invalid_request, existing code) a change oftargetorstop_loss.whenwhile a broker order holds part of the trigger's protection. Broker orders are placed for v2 triggers only withOMS_TRIGGERS_V2_BROKER_PROTECTIONon (off by default: every part showsheld_by_server). API version 1.3.0. New stored record kindtrg2_protection(oms:trg2:protection:{id}, fixtures in read by this release only. Every/v1operation is unchanged. - Additive
/v2/triggers
New operations
create_trigger_v2(POST /v2/triggers),list_triggers_v2(GET /v2/triggers),get_trigger_v2(GET /v2/triggers/{trigger_id}),modify_trigger_v2(PATCH) andcancel_trigger_v2(DELETE), tagtriggers, scopesread/triggersas/v1/triggers, with recorded examples and schemasTriggerV2*,CreateTriggerV2Request,ModifyTriggerV2Request. Writes answer 202 (status: accepted); errors use existing codes only. Only users inOMS_TRIGGERS_V2_USERSmay call them (403 otherwise; unset = nobody). A strategy-levelrisk_limitis refused (422spec.level) until positions are tracked per strategy. API version 1.2.0./v1/triggersand every existing operation are unchanged. - Additive
trigger engine v2 runtime records (no role runs it yet)
The trigger runtime (crate
trigger-runtime, milestone 2) writes, once a role runs it (M3): the bucket mark{v, cmd_seq, epoch}at keybucket/{bucket}in KV{ENV}_TRG_STATE(fixtures in keys{symbol}.{bucket}in KV{ENV}_TRG_SYMBOLS(value: the bucket number), Redisoms:triggers:members/oms:triggers:serves, leasesoms:lease:trg:{bucket}/oms:epoch:trg:{bucket}, and an internal core-NATS noticetrg.ctl.released(payload: the bucket number; not persisted). It readsoms.portfolio.>andoms:portfolio:*:positionsas they are today. No REST, webhook or stream shape changes. - Additive
trigger engine v2 read model and leg product
New stored record kind
trg2_record: the Redis hashoms:trg2:{id}(and index setoms:trg2:user:{user}) that/v2/triggersreads, written by the api (submitted) and by the projector from engine events (newertransition_seqonly). Fixtures and reader test in leg (trg.cmdcreate, KV snapshot) gains an optionalproduct(MIS,CARRYFORWARD,DELIVERY,MTF), absent on older records; a new command fixture shows it with the command'sscope. No existing REST, webhook, stream or stored shape changes. - Additive
trigger engine v2 record shapes (nothing writes them yet)
New stored / NATS record kinds for the v2 trigger engine (spec
docs/superpowers/specs/2026-10-09-trigger-engine-v2-design.md§5.1, §6): the command ontrg.cmd.{bucket}.{user}.{trigger_id}, the snapshot in KV{ENV}_TRG_STATE, the event ontrg.evt.{bucket}.{user}.{trigger_id}and the merged tick on{ENV}.ticks.{symbol}, with hand-written fixtures and writes or reads them yet (milestone 1 is the pure core); no existing REST, webhook, stream or stored shape changes. Before any writer exists, the command gained a requiredscope({user, account, strategy?}): a command without it does not read and is never applied, and the engine refuses a command whose scope is not its target trigger's. The never-written command fixtures were updated to carry it. - Additive
broker safety (protection profile, read-back in observe mode, off by default: `OMS_PROTECTION_READBACK`)
No REST, webhook or stream payload changes. One new refusal, with the existing error:
PATCH /v1/orders/{order_id}changing the quantity of a partly filled bracket / cover exit leg is answered 422invalid_request(field: quantity) before any broker call, unless the broker's meaning of a modified quantity is verified (none is yet); cancel the pending part and place a new order for the rest instead.oms.anomalies.>may carry a newkind,protection_mismatch(a protective order read back after a place or modify is not what was asked).AnomalyEvent.kindis free text, so every reader already reads it; the watchdog now pages on it. Readers first: no pod writes it yet (OMS_PROTECTION_MISMATCH_ANOMALY, off; on in a later release).- Trigger hash (
CIRRUS:TRIGGERS:{id}), broker-held triggers only:capabilitiesgainsprotection(facts: every broker protection fact, its value or"unverified";trail_plan:leg/gtt). Every existing key keeps its value. - New Mongo collection
broker_observations(what a modify's quantity meant at a broker): broker, operation, class (total,remaining,ignored,unexplained, frozen inwire_enums.json), quantities before / after, a keyed hash of the account (account_key, fromOMS_OBSERVATION_KEY), no account or order id.
- Additive
unanswered cancels and modifies are checked against the book
A cancel or modify (
DELETE /v1/orders/{order_id},PATCH /v1/orders/{order_id},DELETE /v1/orders/bracket/{order_id}) the broker never answers still answersstatus: "unknown"as before. The engine is now told about it too (oms.ingestamendedmessage, new optionalunconfirmed: true, left out when false so older pods read the same bytes), reads the order book shortly after even on streamed accounts, and raises the critical anomalyamend_unconfirmed(staff alert, admin anomaly feed) when the order is still open and unchanged. Nothing is re-sent to the broker. No REST field, type, enum value, status code, webhook or stream message changes. - Additive
paper accounts report native bracket, cover and GTT support
GET /v1/accountsnow reportsprotectionbracket,cover,gttandgtt_two_legastruefor Paper accounts, and protected Paper orders use them like any broker that has them: intraday stop-loss + target goes out as a bracket order, stop-loss only as a cover order, positional protection as a GTT (two-leg when both levels are set).GET /v1/gttlists Paper GTTs. No field, enum value or status code changes. - Additive
bracket / cover exit legs named by the broker's leg identity
An order's
leg_role(stop_loss/target/square_off) now comes from the leg identity the broker reports (Pocketfulleg_order_indicator, TradejinilegType, Kite covervariety), never from the order type, which brokers change once a stop triggers or exits. A Tradejini stop leg listed aslimitafter it exited now readsstop_loss(it readtarget), as does each slice of a leg split by a partial fill. An exit leg whose role the broker does not identify carries noleg_role(the field was already optional and is omitted, as for every non-leg order). No field, type, enum value or status code changes. - Additive
more broker refusals classed
Classed from production: Dhan "Invalid SecurityId (DH-906)" is
invalid_field; Fyers "exchange not enabled for account", Motilal "scrip blocked by risk department" and Zerodha "MIS orders are currently blocked for <stock>" arerms_block; a Zerodha GTT trigger too close to the last price isinvalid_price_or_qty. Same refusal texts to users; no field, enum value or status code changes.